diff --git a/README.md b/README.md index 76a4d3f..92059d8 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,13 @@ It supports: - HTML - Text-based formats (CSV, JSON, XML) - ZIP files (iterates over contents) +... and more! -To install MarkItDown, use pip: `pip install markitdown`. Alternatively, you can install it from the source: `pip install -e .` +To install MarkItDown, use pip: `pip install markitdown`. Alternatively, you can install it from the source: + +```bash +pip install -e ./packages/markitdown` +``` ## Usage @@ -33,20 +38,39 @@ Or use `-o` to specify the output file: markitdown path-to-file.pdf -o document.md ``` -To use Document Intelligence conversion: - -```bash -markitdown path-to-file.pdf -o document.md -d -e "" -``` - You can also pipe content: ```bash cat path-to-file.pdf | markitdown ``` +### Plugins + +MarkItDown also supports 3rd-party plugins. Plugins are disabled by default. To list installed plugins: + +```bash +markitdown --list-plugins +``` + +To enable plugins use: + +```bash +markitdown --use-plugins path-to-file.pdf +``` + +To find available plugins, search GitHub for the hashtag `#markitdown-plugin`. + +### Azure Document Intelligence + +To use Microsoft Document Intelligence for conversion: + +```bash +markitdown path-to-file.pdf -o document.md -d -e "" +``` + More information about how to set up an Azure Document Intelligence Resource can be found [here](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/how-to-guides/create-document-intelligence-resource?view=doc-intel-4.0.0) + ### Python API Basic usage in Python: @@ -54,7 +78,7 @@ Basic usage in Python: ```python from markitdown import MarkItDown -md = MarkItDown() +md = MarkItDown(enable_plugins=False) # Set to True to enable plugins result = md.convert("test.xlsx") print(result.text_content) ``` @@ -87,42 +111,6 @@ print(result.text_content) docker build -t markitdown:latest . docker run --rm -i markitdown:latest < ~/your-file.pdf > output.md ``` -
- -Batch Processing Multiple Files - -This example shows how to convert multiple files to markdown format in a single run. The script processes all supported files in a directory and creates corresponding markdown files. - - -```python convert.py -from markitdown import MarkItDown -from openai import OpenAI -import os -client = OpenAI(api_key="your-api-key-here") -md = MarkItDown(llm_client=client, llm_model="gpt-4o-2024-11-20") -supported_extensions = ('.pptx', '.docx', '.pdf', '.jpg', '.jpeg', '.png') -files_to_convert = [f for f in os.listdir('.') if f.lower().endswith(supported_extensions)] -for file in files_to_convert: - print(f"\nConverting {file}...") - try: - md_file = os.path.splitext(file)[0] + '.md' - result = md.convert(file) - with open(md_file, 'w') as f: - f.write(result.text_content) - - print(f"Successfully converted {file} to {md_file}") - except Exception as e: - print(f"Error converting {file}: {str(e)}") - -print("\nAll conversions completed!") -``` -2. Place the script in the same directory as your files -3. Install required packages: like openai -4. Run script ```bash python convert.py ``` - -Note that original files will remain unchanged and new markdown files are created with the same base name. - -
## Contributing @@ -169,6 +157,11 @@ You can help by looking at issues or helping review PRs. Any issue or PR is welc - Run pre-commit checks before submitting a PR: `pre-commit run --all-files` +### Contributing 3rd-party Plugins + +You can also contribute by creating and sharing 3rd party plugins. See `packages/markitdown-sample-plugin` for more details. + + ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft diff --git a/packages/markitdown/README.md b/packages/markitdown/README.md index 76a4d3f..3d52184 100644 --- a/packages/markitdown/README.md +++ b/packages/markitdown/README.md @@ -1,23 +1,25 @@ # MarkItDown -[![PyPI](https://img.shields.io/pypi/v/markitdown.svg)](https://pypi.org/project/markitdown/) -![PyPI - Downloads](https://img.shields.io/pypi/dd/markitdown) -[![Built by AutoGen Team](https://img.shields.io/badge/Built%20by-AutoGen%20Team-blue)](https://github.com/microsoft/autogen) +> [!IMPORTANT] +> MarkItDown is a Python package and command-line utility for converting various files to Markdown (e.g., for indexing, text analysis, etc). +> +> For more information, and full documentation, see the project [README.md](https://github.com/microsoft/autogen) on GitHub. +## Installation -MarkItDown is a utility for converting various files to Markdown (e.g., for indexing, text analysis, etc). -It supports: -- PDF -- PowerPoint -- Word -- Excel -- Images (EXIF metadata and OCR) -- Audio (EXIF metadata and speech transcription) -- HTML -- Text-based formats (CSV, JSON, XML) -- ZIP files (iterates over contents) +From PyPI: -To install MarkItDown, use pip: `pip install markitdown`. Alternatively, you can install it from the source: `pip install -e .` +```bash +pip install markitdown +``` + +From source: + +```bash +git clone git@github.com:microsoft/markitdown.git +cd markitdown +pip install -e packages/markitdown +``` ## Usage @@ -27,30 +29,8 @@ To install MarkItDown, use pip: `pip install markitdown`. Alternatively, you can markitdown path-to-file.pdf > document.md ``` -Or use `-o` to specify the output file: - -```bash -markitdown path-to-file.pdf -o document.md -``` - -To use Document Intelligence conversion: - -```bash -markitdown path-to-file.pdf -o document.md -d -e "" -``` - -You can also pipe content: - -```bash -cat path-to-file.pdf | markitdown -``` - -More information about how to set up an Azure Document Intelligence Resource can be found [here](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/how-to-guides/create-document-intelligence-resource?view=doc-intel-4.0.0) - ### Python API -Basic usage in Python: - ```python from markitdown import MarkItDown @@ -59,115 +39,9 @@ result = md.convert("test.xlsx") print(result.text_content) ``` -Document Intelligence conversion in Python: +### More Information -```python -from markitdown import MarkItDown - -md = MarkItDown(docintel_endpoint="") -result = md.convert("test.pdf") -print(result.text_content) -``` - -To use Large Language Models for image descriptions, provide `llm_client` and `llm_model`: - -```python -from markitdown import MarkItDown -from openai import OpenAI - -client = OpenAI() -md = MarkItDown(llm_client=client, llm_model="gpt-4o") -result = md.convert("example.jpg") -print(result.text_content) -``` - -### Docker - -```sh -docker build -t markitdown:latest . -docker run --rm -i markitdown:latest < ~/your-file.pdf > output.md -``` -
- -Batch Processing Multiple Files - -This example shows how to convert multiple files to markdown format in a single run. The script processes all supported files in a directory and creates corresponding markdown files. - - -```python convert.py -from markitdown import MarkItDown -from openai import OpenAI -import os -client = OpenAI(api_key="your-api-key-here") -md = MarkItDown(llm_client=client, llm_model="gpt-4o-2024-11-20") -supported_extensions = ('.pptx', '.docx', '.pdf', '.jpg', '.jpeg', '.png') -files_to_convert = [f for f in os.listdir('.') if f.lower().endswith(supported_extensions)] -for file in files_to_convert: - print(f"\nConverting {file}...") - try: - md_file = os.path.splitext(file)[0] + '.md' - result = md.convert(file) - with open(md_file, 'w') as f: - f.write(result.text_content) - - print(f"Successfully converted {file} to {md_file}") - except Exception as e: - print(f"Error converting {file}: {str(e)}") - -print("\nAll conversions completed!") -``` -2. Place the script in the same directory as your files -3. Install required packages: like openai -4. Run script ```bash python convert.py ``` - -Note that original files will remain unchanged and new markdown files are created with the same base name. - -
- -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -### How to Contribute - -You can help by looking at issues or helping review PRs. Any issue or PR is welcome, but we have also marked some as 'open for contribution' and 'open for reviewing' to help facilitate community contributions. These are ofcourse just suggestions and you are welcome to contribute in any way you like. - - -
- -| | All | Especially Needs Help from Community | -|-----------------------|------------------------------------------|------------------------------------------------------------------------------------------| -| **Issues** | [All Issues](https://github.com/microsoft/markitdown/issues) | [Issues open for contribution](https://github.com/microsoft/markitdown/issues?q=is%3Aissue+is%3Aopen+label%3A%22open+for+contribution%22) | -| **PRs** | [All PRs](https://github.com/microsoft/markitdown/pulls) | [PRs open for reviewing](https://github.com/microsoft/markitdown/pulls?q=is%3Apr+is%3Aopen+label%3A%22open+for+reviewing%22) | - -
- -### Running Tests and Checks - -- Install `hatch` in your environment and run tests: - ```sh - pip install hatch # Other ways of installing hatch: https://hatch.pypa.io/dev/install/ - hatch shell - hatch test - ``` - - (Alternative) Use the Devcontainer which has all the dependencies installed: - ```sh - # Reopen the project in Devcontainer and run: - hatch test - ``` - -- Run pre-commit checks before submitting a PR: `pre-commit run --all-files` +For more information, and full documentation, see the project [README.md](https://github.com/microsoft/autogen) on GitHub. ## Trademarks diff --git a/packages/markitdown/src/markitdown/__main__.py b/packages/markitdown/src/markitdown/__main__.py index 353be84..6a24391 100644 --- a/packages/markitdown/src/markitdown/__main__.py +++ b/packages/markitdown/src/markitdown/__main__.py @@ -4,6 +4,7 @@ import argparse import sys from textwrap import dedent +from importlib.metadata import entry_points from .__about__ import __version__ from ._markitdown import MarkItDown, DocumentConverterResult @@ -71,9 +72,39 @@ def main(): help="Document Intelligence Endpoint. Required if using Document Intelligence.", ) + parser.add_argument( + "-p", + "--use-plugins", + action="store_true", + help="Use 3rd-party plugins to convert files. Use --list-plugins to see installed plugins.", + ) + + parser.add_argument( + "--list-plugins", + action="store_true", + help="List installed 3rd-party plugins. Plugins are loaded when using the -p or --use-plugin option.", + ) + parser.add_argument("filename", nargs="?") args = parser.parse_args() + if args.list_plugins: + # List installed plugins, then exit + print("Installed MarkItDown 3rd-party Plugins:\n") + plugin_entry_points = list(entry_points(group="markitdown.plugin")) + if len(plugin_entry_points) == 0: + print(" * No 3rd-party plugins installed.") + print( + "\nFind plugins by searching for the hashtag #markitdown-plugin on GitHub.\n" + ) + else: + for entry_point in plugin_entry_points: + print(f" * {entry_point.name:<16}\t(package: {entry_point.value})") + print( + "\nUse the -p (or --use-plugins) option to enable 3rd-party plugins.\n" + ) + sys.exit(0) + if args.use_docintel: if args.endpoint is None: raise ValueError( @@ -81,9 +112,11 @@ def main(): ) elif args.filename is None: raise ValueError("Filename is required when using Document Intelligence.") - markitdown = MarkItDown(docintel_endpoint=args.endpoint) + markitdown = MarkItDown( + enable_plugins=args.use_plugins, docintel_endpoint=args.endpoint + ) else: - markitdown = MarkItDown() + markitdown = MarkItDown(enable_plugins=args.use_plugins) if args.filename is None: result = markitdown.convert_stream(sys.stdin.buffer) diff --git a/packages/markitdown/src/markitdown/_markitdown.py b/packages/markitdown/src/markitdown/_markitdown.py index 2c7ac77..b7ac5bc 100644 --- a/packages/markitdown/src/markitdown/_markitdown.py +++ b/packages/markitdown/src/markitdown/_markitdown.py @@ -11,13 +11,10 @@ from pathlib import Path from urllib.parse import urlparse from warnings import warn - # File-format detection import puremagic import requests -# Azure imports - from .converters import ( DocumentConverter, DocumentConverterResult, @@ -84,96 +81,98 @@ class MarkItDown: def __init__( self, *, - load_plugins: bool = True, - requests_session: Optional[requests.Session] = None, - llm_client: Optional[Any] = None, - llm_model: Optional[str] = None, - style_map: Optional[str] = None, - exiftool_path: Optional[str] = None, - docintel_endpoint: Optional[str] = None, - # Deprecated - mlm_client: Optional[Any] = None, - mlm_model: Optional[str] = None, + enable_builtins: Union[None, bool] = None, + enable_plugins: Union[None, bool] = None, + **kwargs, ): + self._builtins_enabled = False + self._plugins_enabled = False + + requests_session = kwargs.get("requests_session") if requests_session is None: self._requests_session = requests.Session() else: self._requests_session = requests_session - if exiftool_path is None: - exiftool_path = os.environ.get("EXIFTOOL_PATH") - - # Handle deprecation notices - ############################# - if mlm_client is not None: - if llm_client is None: - warn( - "'mlm_client' is deprecated, and was renamed 'llm_client'.", - DeprecationWarning, - ) - llm_client = mlm_client - mlm_client = None - else: - raise ValueError( - "'mlm_client' is deprecated, and was renamed 'llm_client'. Do not use both at the same time. Just use 'llm_client' instead." - ) - - if mlm_model is not None: - if llm_model is None: - warn( - "'mlm_model' is deprecated, and was renamed 'llm_model'.", - DeprecationWarning, - ) - llm_model = mlm_model - mlm_model = None - else: - raise ValueError( - "'mlm_model' is deprecated, and was renamed 'llm_model'. Do not use both at the same time. Just use 'llm_model' instead." - ) - ############################# - - self._llm_client = llm_client - self._llm_model = llm_model - self._style_map = style_map - self._exiftool_path = exiftool_path + # TODO - remove these (see enable_builtins) + self._llm_client = None + self._llm_model = None + self._exiftool_path = None + self._style_map = None + # Register the converters self._page_converters: List[DocumentConverter] = [] - # Register converters for successful browsing operations - # Later registrations are tried first / take higher priority than earlier registrations - # To this end, the most specific converters should appear below the most generic converters - self.register_converter(PlainTextConverter()) - self.register_converter(ZipConverter()) - self.register_converter(HtmlConverter()) - self.register_converter(RssConverter()) - self.register_converter(WikipediaConverter()) - self.register_converter(YouTubeConverter()) - self.register_converter(BingSerpConverter()) - self.register_converter(DocxConverter()) - self.register_converter(XlsxConverter()) - self.register_converter(XlsConverter()) - self.register_converter(PptxConverter()) - self.register_converter(WavConverter()) - self.register_converter(Mp3Converter()) - self.register_converter(ImageConverter()) - self.register_converter(IpynbConverter()) - self.register_converter(PdfConverter()) - self.register_converter(OutlookMsgConverter()) + if ( + enable_builtins is None or enable_builtins + ): # Default to True when not specified + self.enable_builtins(**kwargs) - # Register Document Intelligence converter at the top of the stack if endpoint is provided - if docintel_endpoint is not None: - self.register_converter( - DocumentIntelligenceConverter(endpoint=docintel_endpoint) - ) + if enable_plugins: + self.enable_plugins(**kwargs) - # Load plugins - if load_plugins: + def enable_builtins(self, **kwargs) -> None: + """ + Enable and register built-in converters. + Built-in converters are enabled by default. + This method should only be called once, if built-ins were initially disabled. + """ + if not self._builtins_enabled: + # TODO: Move these into converter constructors + self._llm_client = kwargs.get("llm_client") + self._llm_model = kwargs.get("llm_model") + self._exiftool_path = kwargs.get("exiftool_path") + self._style_map = kwargs.get("style_map") + + # Register converters for successful browsing operations + # Later registrations are tried first / take higher priority than earlier registrations + # To this end, the most specific converters should appear below the most generic converters + self.register_converter(PlainTextConverter()) + self.register_converter(ZipConverter()) + self.register_converter(HtmlConverter()) + self.register_converter(RssConverter()) + self.register_converter(WikipediaConverter()) + self.register_converter(YouTubeConverter()) + self.register_converter(BingSerpConverter()) + self.register_converter(DocxConverter()) + self.register_converter(XlsxConverter()) + self.register_converter(XlsConverter()) + self.register_converter(PptxConverter()) + self.register_converter(WavConverter()) + self.register_converter(Mp3Converter()) + self.register_converter(ImageConverter()) + self.register_converter(IpynbConverter()) + self.register_converter(PdfConverter()) + self.register_converter(OutlookMsgConverter()) + + # Register Document Intelligence converter at the top of the stack if endpoint is provided + docintel_endpoint = kwargs.get("docintel_endpoint") + if docintel_endpoint is not None: + self.register_converter( + DocumentIntelligenceConverter(endpoint=docintel_endpoint) + ) + + self._builtins_enabled = True + else: + warn("Built-in converters are already enabled.", RuntimeWarning) + + def enable_plugins(self, **kwargs) -> None: + """ + Enable and register converters provided by plugins. + Plugins are disabled by default. + This method should only be called once, if plugins were initially disabled. + """ + if not self._plugins_enabled: + # Load plugins for plugin in _load_plugins(): try: - plugin.register_converters(self) + plugin.register_converters(self, **kwargs) except Exception: tb = traceback.format_exc() warn(f"Plugin '{plugin}' failed to register converters:\n{tb}") + self._plugins_enabled = True + else: + warn("Plugins converters are already enabled.", RuntimeWarning) def convert( self, source: Union[str, requests.Response, Path], **kwargs: Any