diff --git a/openrag/components/indexer/loaders/pptx_loader.py b/openrag/components/indexer/loaders/pptx_loader.py index 887a94b56..67364aae7 100644 --- a/openrag/components/indexer/loaders/pptx_loader.py +++ b/openrag/components/indexer/loaders/pptx_loader.py @@ -1,15 +1,25 @@ import html import re from io import BytesIO + import pptx +from html_to_markdown import convert from langchain_core.documents.base import Document from PIL import Image from tqdm.asyncio import tqdm +from utils.logger import get_logger from .base import BaseLoader +logger = get_logger() + class PPTXConverter: + """Implementation based on PPTX converter in MarkItDown library. + + https://github.com/microsoft/markitdown/blob/main/packages/markitdown/src/markitdown/converters/_pptx_converter.py + """ + def __init__( self, image_placeholder=r"", page_separator: str = "[PAGE_SEP]" ): @@ -44,9 +54,7 @@ def convert(self, local_path): html_table += "" first_row = False html_table += "" - md_content += ( - "\n" + self._convert(html_table).text_content.strip() + "\n" - ) + md_content += "\n" + convert(html_table).text_content.strip() + "\n" # Charts if shape.has_chart: @@ -73,40 +81,59 @@ def convert(self, local_path): return md_content, images_list def _is_picture(self, shape): - if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PICTURE: - return True - if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PLACEHOLDER: - if hasattr(shape, "image"): + try: + if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PICTURE: return True + if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PLACEHOLDER: + if hasattr(shape, "image"): + return True + except NotImplementedError: + # https://python-pptx.readthedocs.io/en/latest/_modules/pptx/shapes/autoshape.html + # Not all shape types are implemented in python-pptx + logger.warning("Encountered an unimplemented shape type.") + return False def _is_table(self, shape): - if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.TABLE: - return True + try: + if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.TABLE: + return True + except NotImplementedError: + # # https://python-pptx.readthedocs.io/en/latest/_modules/pptx/shapes/autoshape.html + # Not all shape types are implemented in python-pptx + logger.warning("Encountered an unimplemented shape type.") return False def _convert_chart_to_markdown(self, chart): - md = "\n\n### Chart" - if chart.has_title: - md += f": {chart.chart_title.text_frame.text}" - md += "\n\n" - data = [] - category_names = [c.label for c in chart.plots[0].categories] - series_names = [s.name for s in chart.series] - data.append(["Category"] + series_names) - - for idx, category in enumerate(category_names): - row = [category] - for series in chart.series: - row.append(series.values[idx]) - data.append(row) - - markdown_table = [] - for row in data: - markdown_table.append("| " + " | ".join(map(str, row)) + " |") - header = markdown_table[0] - separator = "|" + "|".join(["---"] * len(data[0])) + "|" - return md + "\n".join([header, separator] + markdown_table[1:]) + try: + md = "\n\n### Chart" + if chart.has_title: + md += f": {chart.chart_title.text_frame.text}" + md += "\n\n" + data = [] + category_names = [c.label for c in chart.plots[0].categories] + series_names = [s.name for s in chart.series] + data.append(["Category"] + series_names) + + for idx, category in enumerate(category_names): + row = [category] + for series in chart.series: + row.append(series.values[idx]) + data.append(row) + + markdown_table = [] + for row in data: + markdown_table.append("| " + " | ".join(map(str, row)) + " |") + header = markdown_table[0] + separator = "|" + "|".join(["---"] * len(data[0])) + "|" + return md + "\n".join([header, separator] + markdown_table[1:]) + except ValueError as e: + # Handle the specific error for unsupported chart types + if "unsupported plot type" in str(e): + return "\n\n[unsupported chart]\n\n" + except Exception: + # Catch any other exceptions that might occur + return "\n\n[unsupported chart]\n\n" class PPTXLoader(BaseLoader): diff --git a/pyproject.toml b/pyproject.toml index 90805c55b..e097ba0cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "hdbscan>=0.8.40", "pytest-env>=1.1.5", "markitdown[docx]>=0.1.3", + "html-to-markdown>=2.4.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index f4b103852..47a64b717 100644 --- a/uv.lock +++ b/uv.lock @@ -990,7 +990,9 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/c1/84/6b010387b795f774e1ec695df3c8660c15abd041783647d5e7e4076bfc6b/hdbscan-0.8.40.tar.gz", hash = "sha256:c9e383ff17beee0591075ff65d524bda5b5a35dfb01d218245a7ba30c8d48a17", size = 6904096 } wheels = [ { url = "https://files.pythonhosted.org/packages/33/ff/4739886abb990dc6feb7b02eafb38a7eaf090fffef6336e70a03d693f433/hdbscan-0.8.40-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:353eaa22e42bee69df095744dbb8b29360e516bd9dcb84580dceeeb755f004cc", size = 1497291 }, + { url = "https://files.pythonhosted.org/packages/f0/0f/97a315772abf99b3c230e3d57f3fa426d163ce4d6070241d68a3f0241ea9/hdbscan-0.8.40-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:991e745aa51abfb8abfb0e1525b9309df03a2f67fdd8df96e18f91fe7fe06806", size = 4362227 }, { url = "https://files.pythonhosted.org/packages/c0/cb/6b4254f8a33e075118512e55acf3485c155ea52c6c35d69a985bdc59297c/hdbscan-0.8.40-cp312-cp312-win_amd64.whl", hash = "sha256:1b55a935ed7b329adac52072e1c4028979dfc54312ca08de2deece9c97d6ebb1", size = 726198 }, + { url = "https://files.pythonhosted.org/packages/80/2d/ca4d81a5aa5b8ccde1d41b892a2f480c2df238c58f113eee4df50473a3b0/hdbscan-0.8.40-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:32ea7bc4ce8854b5549d341edc841a29766feb62f8c399520e6e0940a41c5e39", size = 4336968 }, ] [[package]] @@ -1017,6 +1019,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, ] +[[package]] +name = "html-to-markdown" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/3b/087769ea5ca7d5a48f7fcc06a7bb20825de0aec6caa42ef408b3598ca1c2/html_to_markdown-2.4.0.tar.gz", hash = "sha256:d77cad62eeafc1ae86875963505d7ce307ea6bdb88571d15c0dcef1546f6948a", size = 1906223 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/08/e72ef1a9472003f6725a67bf16f4d28687ede9e71cdbe4aa75474b99eed7/html_to_markdown-2.4.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:edbc4889746024f8f0b812770358119e2adfba5fdf35a8af5da3df4fa0cf5efc", size = 4814069 }, + { url = "https://files.pythonhosted.org/packages/83/b6/8ae4514c4b74678ecc9cbf865aa2dd19436b78061b7dbe38b767e5049b96/html_to_markdown-2.4.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b673ee764693350697b956258f66fac57c48ff5ff48ff4da374e211e1c86472", size = 5334332 }, + { url = "https://files.pythonhosted.org/packages/e5/95/8bcf05476db7fc3359fe55933bbafb4ff3051c6c80ca71e90e5fc8a8c425/html_to_markdown-2.4.0-cp310-abi3-win_amd64.whl", hash = "sha256:33b59ebd528649cfeccf8dcf442b198b29f5b609ea8153f32e1970293c51383d", size = 5082797 }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -2379,6 +2392,7 @@ dependencies = [ { name = "einops" }, { name = "eml-parser" }, { name = "hdbscan" }, + { name = "html-to-markdown" }, { name = "hydra-core" }, { name = "infinity-client" }, { name = "langchain-community" }, @@ -2427,6 +2441,7 @@ requires-dist = [ { name = "einops", specifier = ">=0.8.1" }, { name = "eml-parser", specifier = ">=2.0.0" }, { name = "hdbscan", specifier = ">=0.8.40" }, + { name = "html-to-markdown", specifier = ">=2.4.0" }, { name = "hydra-core", specifier = ">=1.3.2" }, { name = "infinity-client", specifier = ">=0.0.76" }, { name = "langchain-community", specifier = ">=0.3.18" },