markitdown-pro 0.1.0__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.
- markitdown_pro/__init__.py +1 -0
- markitdown_pro/common/__init__.py +0 -0
- markitdown_pro/common/logger.py +6 -0
- markitdown_pro/common/utils.py +59 -0
- markitdown_pro/conversion_pipeline.py +212 -0
- markitdown_pro/converters/__init__.py +0 -0
- markitdown_pro/converters/azure_docint.py +13 -0
- markitdown_pro/converters/base.py +26 -0
- markitdown_pro/converters/gpt4o_mini_vision.py +21 -0
- markitdown_pro/converters/markitdown_wrapper.py +44 -0
- markitdown_pro/converters/pymupdf_wrapper.py +42 -0
- markitdown_pro/converters/unstructured_wrapper.py +62 -0
- markitdown_pro/converters/youtube_wrapper.py +67 -0
- markitdown_pro/handlers/__init__.py +0 -0
- markitdown_pro/handlers/audio_handler.py +40 -0
- markitdown_pro/handlers/base_handler.py +16 -0
- markitdown_pro/handlers/email_handler.py +169 -0
- markitdown_pro/handlers/epub_handler.py +33 -0
- markitdown_pro/handlers/image_handler.py +48 -0
- markitdown_pro/handlers/ipynb_handler.py +31 -0
- markitdown_pro/handlers/markup_handler.py +75 -0
- markitdown_pro/handlers/office_handler.py +47 -0
- markitdown_pro/handlers/pdf_handler.py +122 -0
- markitdown_pro/handlers/pst_handler.py +153 -0
- markitdown_pro/handlers/tabular_handler.py +34 -0
- markitdown_pro/handlers/text_handler.py +38 -0
- markitdown_pro/services/__init__.py +0 -0
- markitdown_pro/services/azure_service.py +160 -0
- markitdown_pro/services/openai_services.py +209 -0
- markitdown_pro-0.1.0.dist-info/METADATA +367 -0
- markitdown_pro-0.1.0.dist-info/RECORD +33 -0
- markitdown_pro-0.1.0.dist-info/WHEEL +5 -0
- markitdown_pro-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import common, converters, handlers, services # noqa F401
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def detect_extension(file_path: str) -> str:
|
|
6
|
+
"""Return the file extension in lowercase."""
|
|
7
|
+
return Path(file_path).suffix.lower()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_pdf(file_path: str) -> bool:
|
|
11
|
+
"""Check if the file is a PDF."""
|
|
12
|
+
return detect_extension(file_path) == ".pdf"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_image(file_path: str) -> bool:
|
|
16
|
+
"""Check if the file is an image based on its extension."""
|
|
17
|
+
return detect_extension(file_path) in (
|
|
18
|
+
".jpg",
|
|
19
|
+
".jpeg",
|
|
20
|
+
".png",
|
|
21
|
+
".bmp",
|
|
22
|
+
".tiff",
|
|
23
|
+
".gif",
|
|
24
|
+
".heic",
|
|
25
|
+
".webp",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_audio(file_path: str) -> bool:
|
|
30
|
+
"""Check if the file is an audio file based on its extension."""
|
|
31
|
+
return detect_extension(file_path) in (".mp3", ".wav", ".m4a", ".ogg", ".flac")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_zip(file_path: str) -> bool:
|
|
35
|
+
"""Check if the file is a ZIP archive."""
|
|
36
|
+
return detect_extension(file_path) == ".zip"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_eml(file_path: str) -> bool:
|
|
40
|
+
"""Check if the file is an EML email file."""
|
|
41
|
+
return detect_extension(file_path) == ".eml"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def clean_markdown(md_text: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Clean up Markdown text by removing trailing spaces and reducing excess newlines.
|
|
47
|
+
"""
|
|
48
|
+
md_text = re.sub(r"[ \t]+(\r?\n)", r"\1", md_text)
|
|
49
|
+
md_text = re.sub(r"\n{3,}", "\n\n", md_text)
|
|
50
|
+
return md_text.strip()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ensure_minimum_content(md_text: str) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Check if the Markdown text has non-trivial content.
|
|
56
|
+
"""
|
|
57
|
+
if not md_text:
|
|
58
|
+
return False
|
|
59
|
+
return bool(md_text and len(md_text.strip()) > 30)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .common.logger import logger
|
|
9
|
+
from .common.utils import detect_extension, ensure_minimum_content, is_image
|
|
10
|
+
from .converters.azure_docint import AzureDocIntWrapper
|
|
11
|
+
from .converters.gpt4o_mini_vision import GPT4oMiniVisionWrapper
|
|
12
|
+
from .converters.markitdown_wrapper import MarkitDownWrapper
|
|
13
|
+
from .converters.unstructured_wrapper import UnstructuredWrapper
|
|
14
|
+
from .handlers.audio_handler import AudioHandler
|
|
15
|
+
from .handlers.base_handler import BaseHandler # Import BaseHandler
|
|
16
|
+
from .handlers.email_handler import EmailHandler
|
|
17
|
+
from .handlers.epub_handler import EPUBHandler
|
|
18
|
+
from .handlers.image_handler import ImageHandler
|
|
19
|
+
from .handlers.ipynb_handler import IpynbHandler
|
|
20
|
+
from .handlers.markup_handler import MarkupHandler
|
|
21
|
+
from .handlers.office_handler import OfficeHandler
|
|
22
|
+
from .handlers.pdf_handler import PDFHandler
|
|
23
|
+
from .handlers.pst_handler import PSTHandler
|
|
24
|
+
from .handlers.tabular_handler import TabularHandler
|
|
25
|
+
from .handlers.text_handler import TextHandler
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _write_md(path: str, content: str) -> str:
|
|
29
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
30
|
+
f.write(content)
|
|
31
|
+
return content
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConversionPipeline:
|
|
35
|
+
"""
|
|
36
|
+
A class to encapsulate the conversion pipeline, making it easier to use
|
|
37
|
+
without needing to directly interact with dependency injection in the main
|
|
38
|
+
functions. This simplifies testing and usage.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
):
|
|
44
|
+
self.pdf_handler = PDFHandler()
|
|
45
|
+
self.audio_handler = AudioHandler()
|
|
46
|
+
self.image_handler = ImageHandler()
|
|
47
|
+
self.text_handler = TextHandler()
|
|
48
|
+
self.tabular_handler = TabularHandler()
|
|
49
|
+
self.markup_handler = MarkupHandler()
|
|
50
|
+
self.office_handler = OfficeHandler()
|
|
51
|
+
self.epub_handler = EPUBHandler()
|
|
52
|
+
self.email_handler = EmailHandler()
|
|
53
|
+
self.pst_handler = PSTHandler()
|
|
54
|
+
self.ipynb_handler = IpynbHandler()
|
|
55
|
+
self.markitdown_wrapper = MarkitDownWrapper()
|
|
56
|
+
self.unstructured_wrapper = UnstructuredWrapper()
|
|
57
|
+
self.azure_docint_wrapper = AzureDocIntWrapper()
|
|
58
|
+
self.gpt4o_mini_vision_wrapper = GPT4oMiniVisionWrapper()
|
|
59
|
+
|
|
60
|
+
# Define the handlers in a dictionary mapping extensions to handlers
|
|
61
|
+
self.handlers_mapping: dict[str, BaseHandler] = { # Use BaseHandler type hint
|
|
62
|
+
".pdf": self.pdf_handler,
|
|
63
|
+
".mp3": self.audio_handler,
|
|
64
|
+
".wav": self.audio_handler,
|
|
65
|
+
".ogg": self.audio_handler,
|
|
66
|
+
".flac": self.audio_handler,
|
|
67
|
+
".m4a": self.audio_handler,
|
|
68
|
+
".aac": self.audio_handler,
|
|
69
|
+
".wma": self.audio_handler,
|
|
70
|
+
".webm": self.audio_handler,
|
|
71
|
+
".opus": self.audio_handler,
|
|
72
|
+
".bmp": self.image_handler,
|
|
73
|
+
".gif": self.image_handler,
|
|
74
|
+
".heic": self.image_handler,
|
|
75
|
+
".jpeg": self.image_handler,
|
|
76
|
+
".jpg": self.image_handler,
|
|
77
|
+
".png": self.image_handler,
|
|
78
|
+
".prn": self.image_handler,
|
|
79
|
+
".svg": self.image_handler,
|
|
80
|
+
".tiff": self.image_handler,
|
|
81
|
+
".webp": self.image_handler,
|
|
82
|
+
".heif": self.image_handler,
|
|
83
|
+
".txt": self.text_handler,
|
|
84
|
+
".md": self.text_handler,
|
|
85
|
+
".py": self.text_handler,
|
|
86
|
+
".go": self.text_handler,
|
|
87
|
+
".csv": self.tabular_handler,
|
|
88
|
+
".tsv": self.tabular_handler,
|
|
89
|
+
".xls": self.tabular_handler,
|
|
90
|
+
".xlsx": self.tabular_handler,
|
|
91
|
+
".html": self.markup_handler,
|
|
92
|
+
".htm": self.markup_handler,
|
|
93
|
+
".xml": self.markup_handler,
|
|
94
|
+
".json": self.markup_handler,
|
|
95
|
+
".ndjson": self.markup_handler,
|
|
96
|
+
".yaml": self.markup_handler,
|
|
97
|
+
".yml": self.markup_handler,
|
|
98
|
+
".doc": self.office_handler,
|
|
99
|
+
".docx": self.office_handler,
|
|
100
|
+
".odt": self.office_handler,
|
|
101
|
+
".rtf": self.office_handler,
|
|
102
|
+
".ppt": self.office_handler,
|
|
103
|
+
".pptx": self.office_handler,
|
|
104
|
+
".epub": self.epub_handler,
|
|
105
|
+
".eml": self.email_handler,
|
|
106
|
+
".p7s": self.email_handler,
|
|
107
|
+
".msg": self.email_handler,
|
|
108
|
+
".pst": self.pst_handler,
|
|
109
|
+
".ipynb": self.ipynb_handler,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async def convert_document_to_md(self, file_path: str, output_md: Optional[str] = None) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Convert a document to Markdown by trying a series of specialized handlers.
|
|
115
|
+
"""
|
|
116
|
+
if not Path(file_path).is_file():
|
|
117
|
+
raise ValueError(f"The provided path '{file_path}' is not a valid file.")
|
|
118
|
+
|
|
119
|
+
if not output_md:
|
|
120
|
+
base = os.path.splitext(file_path)[0]
|
|
121
|
+
output_md = base + ".md"
|
|
122
|
+
|
|
123
|
+
logger.debug(f"convert_document_to_md: Converting '{file_path}' --> '{output_md}'")
|
|
124
|
+
|
|
125
|
+
extension = detect_extension(file_path).lower()
|
|
126
|
+
|
|
127
|
+
handler = self.handlers_mapping.get(extension)
|
|
128
|
+
|
|
129
|
+
if handler:
|
|
130
|
+
try:
|
|
131
|
+
logger.debug(f"convert_document_to_md: Using handler for extension '{extension}'")
|
|
132
|
+
md_content = await handler.handle(file_path) # AWAIT the handler!
|
|
133
|
+
if not md_content:
|
|
134
|
+
raise RuntimeError(f"Handler for {extension} returned None for {file_path}")
|
|
135
|
+
|
|
136
|
+
logger.debug(
|
|
137
|
+
f"convert_document_to_md: Handler returned {len(md_content)} characters"
|
|
138
|
+
)
|
|
139
|
+
if md_content and ensure_minimum_content(md_content):
|
|
140
|
+
return _write_md(output_md, md_content)
|
|
141
|
+
else:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"Conversion failed for {file_path} using handler for {extension}"
|
|
144
|
+
)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"Error in handler for extension {extension}: {e}")
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
else:
|
|
150
|
+
logger.warning(f"No specific handler found for '{extension}'. Using default path.")
|
|
151
|
+
# Use the injected wrappers directly
|
|
152
|
+
md_out = await self.markitdown_wrapper.process(file_path)
|
|
153
|
+
if md_out and ensure_minimum_content(md_out):
|
|
154
|
+
return _write_md(output_md, md_out)
|
|
155
|
+
md_out = await self.unstructured_wrapper.process(file_path)
|
|
156
|
+
if md_out and ensure_minimum_content(md_out):
|
|
157
|
+
return _write_md(output_md, md_out)
|
|
158
|
+
md_out = await self.azure_docint_wrapper.process(file_path)
|
|
159
|
+
if md_out and ensure_minimum_content(md_out):
|
|
160
|
+
return _write_md(output_md, md_out)
|
|
161
|
+
if is_image(file_path):
|
|
162
|
+
md_out = await self.gpt4o_mini_vision_wrapper.process(file_path)
|
|
163
|
+
if md_out and ensure_minimum_content(md_out):
|
|
164
|
+
return _write_md(output_md, md_out)
|
|
165
|
+
|
|
166
|
+
raise RuntimeError(f"Markdown conversion failed for {file_path} at the default path.")
|
|
167
|
+
|
|
168
|
+
async def convert_document_from_url(
|
|
169
|
+
self, url: str, output_md: Optional[str] = None
|
|
170
|
+
) -> str: # Add async
|
|
171
|
+
logger.info(f"convert_document_from_url: Downloading '{url}'")
|
|
172
|
+
resp = requests.get(url, stream=True) # requests is *synchronous*
|
|
173
|
+
resp.raise_for_status()
|
|
174
|
+
|
|
175
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".download") as tmp:
|
|
176
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
177
|
+
tmp.write(chunk)
|
|
178
|
+
tmp.flush()
|
|
179
|
+
local_path = tmp.name
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
return await self.convert_document_to_md(
|
|
183
|
+
local_path, output_md=output_md
|
|
184
|
+
) # Await the call
|
|
185
|
+
finally:
|
|
186
|
+
Path(local_path).unlink(missing_ok=True)
|
|
187
|
+
|
|
188
|
+
async def convert_document_from_stream(
|
|
189
|
+
self, stream, extension: str, output_md: Optional[str] = None
|
|
190
|
+
) -> str: # Add async
|
|
191
|
+
if not extension.startswith("."):
|
|
192
|
+
extension = "." + extension
|
|
193
|
+
logger.debug(
|
|
194
|
+
f"convert_document_from_url: Converting from stream with extension '{extension}'"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
from io import BytesIO
|
|
198
|
+
|
|
199
|
+
if not isinstance(stream, BytesIO):
|
|
200
|
+
raise ValueError("Stream must be a BytesIO object")
|
|
201
|
+
|
|
202
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=extension) as tmp:
|
|
203
|
+
tmp.write(stream.read())
|
|
204
|
+
tmp.flush()
|
|
205
|
+
local_path = tmp.name
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
return await self.convert_document_to_md(
|
|
209
|
+
local_path, output_md=output_md
|
|
210
|
+
) # Await the call
|
|
211
|
+
finally:
|
|
212
|
+
Path(local_path).unlink(missing_ok=True)
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from ..services.azure_service import AzureServices
|
|
4
|
+
from .base import ConverterWrapper
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AzureDocIntWrapper(ConverterWrapper):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__("Azure Document Intelligence")
|
|
10
|
+
self.azure_services = AzureServices()
|
|
11
|
+
|
|
12
|
+
async def convert(self, file_path: str) -> Optional[str]:
|
|
13
|
+
return self.azure_services.process_azure_doc_intelligence(file_path)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from ..common.logger import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConverterWrapper:
|
|
7
|
+
SUPPORTED_FORMATS = ()
|
|
8
|
+
|
|
9
|
+
def __init__(self, name: str):
|
|
10
|
+
self.name = name
|
|
11
|
+
|
|
12
|
+
async def convert(self, file_path: str) -> Optional[str]: # MUST be async
|
|
13
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
14
|
+
|
|
15
|
+
async def process(self, file_path: str) -> Optional[str]: # Make process async
|
|
16
|
+
logger.info(f"Processing {self.name} for file: {file_path}")
|
|
17
|
+
try:
|
|
18
|
+
result = await self.convert(file_path) # AWAIT the convert
|
|
19
|
+
if result:
|
|
20
|
+
return result
|
|
21
|
+
else:
|
|
22
|
+
logger.warning(f"{self.name} conversion returned insufficient content.")
|
|
23
|
+
return None
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.error(f"Error during {self.name} conversion: {e}")
|
|
26
|
+
return None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from ..common.utils import is_pdf
|
|
4
|
+
from ..services.openai_services import GPT4oMiniVision
|
|
5
|
+
from .base import ConverterWrapper
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GPT4oMiniVisionWrapper(ConverterWrapper):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__("GPT-4o-mini Vision")
|
|
11
|
+
self.gpt4o_mini = GPT4oMiniVision() # Instantiate the actual service
|
|
12
|
+
|
|
13
|
+
async def convert(self, file_path: str) -> Optional[str]:
|
|
14
|
+
if is_pdf(file_path):
|
|
15
|
+
# Try concurrent first, then simple if it fails
|
|
16
|
+
result = await self.gpt4o_mini.process_scanned_pdf_concurrent(file_path)
|
|
17
|
+
if result:
|
|
18
|
+
return result
|
|
19
|
+
return await self.gpt4o_mini.process_scanned_pdf_simple(file_path)
|
|
20
|
+
else: # Assume it's an image
|
|
21
|
+
return await self.gpt4o_mini.process_image(file_path)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from markitdown import MarkItDown
|
|
5
|
+
|
|
6
|
+
from ..common.logger import logger
|
|
7
|
+
from ..common.utils import clean_markdown, ensure_minimum_content
|
|
8
|
+
from .base import ConverterWrapper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MarkitDownWrapper(ConverterWrapper):
|
|
12
|
+
SUPPORTED_FORMATS = [
|
|
13
|
+
"txt",
|
|
14
|
+
"md",
|
|
15
|
+
"html",
|
|
16
|
+
"pdf",
|
|
17
|
+
"docx",
|
|
18
|
+
"xlsx",
|
|
19
|
+
"pptx",
|
|
20
|
+
"zip",
|
|
21
|
+
"wav",
|
|
22
|
+
"mp3",
|
|
23
|
+
"jpg",
|
|
24
|
+
"png",
|
|
25
|
+
"gif",
|
|
26
|
+
"bmp",
|
|
27
|
+
"tiff",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__("MarkItDown")
|
|
32
|
+
self.markitdown = MarkItDown()
|
|
33
|
+
|
|
34
|
+
async def convert(self, file_path: str) -> Optional[str]:
|
|
35
|
+
file_extension = file_path.split(".")[-1].lower()
|
|
36
|
+
logging.info(f"Processing MarkItDown file: {file_path} with extension: {file_extension}")
|
|
37
|
+
|
|
38
|
+
if file_extension not in self.SUPPORTED_FORMATS:
|
|
39
|
+
logger.warning(f"Unsupported file format: {file_extension}")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
result = self.markitdown.convert(file_path)
|
|
43
|
+
text_md = clean_markdown(result.text_content or None)
|
|
44
|
+
return text_md if ensure_minimum_content(text_md) else None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import fitz # PyMuPDF
|
|
5
|
+
|
|
6
|
+
from ..common.logger import logger
|
|
7
|
+
from ..common.utils import clean_markdown, ensure_minimum_content
|
|
8
|
+
from .base import ConverterWrapper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PyMuPDFWrapper(ConverterWrapper):
|
|
12
|
+
SUPPORTED_FORMATS = ["pdf", "xps", "epub", "mobi", "fb2", "cbz", "svg"]
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
super().__init__("PyMuPDF")
|
|
16
|
+
|
|
17
|
+
async def convert(self, file_path: str) -> Optional[str]:
|
|
18
|
+
file_extension = file_path.split(".")[-1].lower()
|
|
19
|
+
logging.info(f"Processing PyMuPDF file: {file_path} with extension: {file_extension}")
|
|
20
|
+
|
|
21
|
+
if file_extension not in self.SUPPORTED_FORMATS:
|
|
22
|
+
logger.warning(f"Unsupported file format: {file_extension}")
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
doc = fitz.open(file_path)
|
|
27
|
+
|
|
28
|
+
# Extract text from all pages
|
|
29
|
+
text_content = ""
|
|
30
|
+
for page_num in range(len(doc)):
|
|
31
|
+
page = doc.load_page(page_num)
|
|
32
|
+
text_content += page.get_text()
|
|
33
|
+
text_content += "\n\n"
|
|
34
|
+
|
|
35
|
+
doc.close()
|
|
36
|
+
|
|
37
|
+
text_md = clean_markdown(text_content)
|
|
38
|
+
return text_md if ensure_minimum_content(text_md) else None
|
|
39
|
+
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Error processing document with PyMuPDF: {e}")
|
|
42
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from unstructured.partition.auto import partition
|
|
4
|
+
|
|
5
|
+
from ..common.logger import logger
|
|
6
|
+
from ..common.utils import clean_markdown, ensure_minimum_content
|
|
7
|
+
from .base import ConverterWrapper
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnstructuredWrapper(ConverterWrapper):
|
|
11
|
+
SUPPORTED_FORMATS = (
|
|
12
|
+
"txt",
|
|
13
|
+
"md",
|
|
14
|
+
"html",
|
|
15
|
+
"htm",
|
|
16
|
+
"xml",
|
|
17
|
+
"pdf",
|
|
18
|
+
"docx",
|
|
19
|
+
"xlsx",
|
|
20
|
+
"pptx",
|
|
21
|
+
"odt",
|
|
22
|
+
"ods",
|
|
23
|
+
"odp",
|
|
24
|
+
"rtf",
|
|
25
|
+
"json",
|
|
26
|
+
"csv",
|
|
27
|
+
"tsv",
|
|
28
|
+
"jpg",
|
|
29
|
+
"jpeg",
|
|
30
|
+
"png",
|
|
31
|
+
"gif",
|
|
32
|
+
"bmp",
|
|
33
|
+
"tiff",
|
|
34
|
+
"svg",
|
|
35
|
+
"mp3",
|
|
36
|
+
"wav",
|
|
37
|
+
"ogg",
|
|
38
|
+
"flac",
|
|
39
|
+
"zip",
|
|
40
|
+
"tar",
|
|
41
|
+
"gz",
|
|
42
|
+
"rar",
|
|
43
|
+
"epub",
|
|
44
|
+
"eml",
|
|
45
|
+
"msg",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
super().__init__("Unstructured")
|
|
50
|
+
|
|
51
|
+
async def convert(self, file_path: str) -> Optional[str]:
|
|
52
|
+
logger.debug(f"Converting {file_path} to markdown")
|
|
53
|
+
file_extension = file_path.split(".")[-1].lower()
|
|
54
|
+
|
|
55
|
+
if file_extension not in self.SUPPORTED_FORMATS:
|
|
56
|
+
logger.warning(f"Unsupported file format: {file_extension}")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Directly call partition. No asyncio.to_thread
|
|
60
|
+
elements = partition(filename=file_path, extract_images_in_pdf=True)
|
|
61
|
+
combined = clean_markdown("\n\n".join(str(el) for el in elements))
|
|
62
|
+
return combined if ensure_minimum_content(combined) else None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from urllib.parse import parse_qs, urlparse
|
|
3
|
+
|
|
4
|
+
from markitdown import MarkItDown
|
|
5
|
+
from youtube_transcript_api import YouTubeTranscriptApi
|
|
6
|
+
|
|
7
|
+
from ..common.logger import logger
|
|
8
|
+
from .base import ConverterWrapper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class YouTubeWrapper(ConverterWrapper):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__("YouTube")
|
|
14
|
+
self.markitdown = MarkItDown()
|
|
15
|
+
|
|
16
|
+
async def convert(self, url: str) -> Optional[str]:
|
|
17
|
+
logger.info(f"Processing YouTube video: {url}")
|
|
18
|
+
|
|
19
|
+
markitdown_result = self.markitdown.convert(url)
|
|
20
|
+
markitdown_content = markitdown_result.text_content if markitdown_result else None
|
|
21
|
+
|
|
22
|
+
if not markitdown_content:
|
|
23
|
+
logger.warning("MarkItDown processing failed or returned empty content.")
|
|
24
|
+
|
|
25
|
+
parsed_url = urlparse(url)
|
|
26
|
+
params = parse_qs(parsed_url.query)
|
|
27
|
+
if "v" not in params:
|
|
28
|
+
logger.warning(f"Invalid YouTube URL: {url}")
|
|
29
|
+
return markitdown_content
|
|
30
|
+
|
|
31
|
+
video_id = params["v"][0]
|
|
32
|
+
try:
|
|
33
|
+
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
|
|
34
|
+
transcript_text = None
|
|
35
|
+
|
|
36
|
+
for lang in [
|
|
37
|
+
"en",
|
|
38
|
+
"es",
|
|
39
|
+
"fr",
|
|
40
|
+
"de",
|
|
41
|
+
"it",
|
|
42
|
+
"pt",
|
|
43
|
+
"ru",
|
|
44
|
+
"zh-Hans",
|
|
45
|
+
"ja",
|
|
46
|
+
"ko",
|
|
47
|
+
]:
|
|
48
|
+
try:
|
|
49
|
+
transcript = transcript_list.find_transcript([lang])
|
|
50
|
+
transcript_text = "\n".join([part["text"] for part in transcript.fetch()])
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.info(f"No transcript found for language {lang}: {e}")
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
if not transcript_text:
|
|
56
|
+
logger.warning(f"No transcript found for YouTube video: {url}")
|
|
57
|
+
return markitdown_content
|
|
58
|
+
full_content = (
|
|
59
|
+
f"{markitdown_content}\n\n# YouTube Transcript\n\n{transcript_text}"
|
|
60
|
+
if markitdown_content
|
|
61
|
+
else f"# YouTube Transcript\n\n{transcript_text}"
|
|
62
|
+
)
|
|
63
|
+
return full_content
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Error fetching transcript for YouTube video {url}: {e}", exc_info=True)
|
|
67
|
+
return markitdown_content
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from ..common.logger import logger
|
|
2
|
+
from ..converters.markitdown_wrapper import MarkitDownWrapper
|
|
3
|
+
from ..handlers.base_handler import BaseHandler
|
|
4
|
+
from ..services.azure_service import AzureServices
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AudioHandler(BaseHandler):
|
|
8
|
+
extensions = {".mp3", ".wav", ".m4a", ".ogg", ".flac"}
|
|
9
|
+
|
|
10
|
+
def __init__(self, *args, **kwargs):
|
|
11
|
+
super().__init__(*args, **kwargs)
|
|
12
|
+
self.markitdown = MarkitDownWrapper()
|
|
13
|
+
self.azure_services = AzureServices()
|
|
14
|
+
|
|
15
|
+
async def handle(self, file_path, *args, **kwargs) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Transcribe an audio file using MarkItDown first.
|
|
18
|
+
If MarkItDown fails, fall back to Azure Speech-to-Text API.
|
|
19
|
+
"""
|
|
20
|
+
logger.info(f"Processing audio file: {file_path} using MarkItDown")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
text: str = await self.markitdown.convert(file_path)
|
|
24
|
+
if text:
|
|
25
|
+
return text
|
|
26
|
+
else:
|
|
27
|
+
logger.warning(
|
|
28
|
+
"MarkItDown conversion returned insufficient content. Falling back to Azure Speech-to-Text API."
|
|
29
|
+
)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logger.error(f"Error processing audio file with MarkItDown: {e}")
|
|
32
|
+
|
|
33
|
+
# Fallback to Azure Speech-to-Text API
|
|
34
|
+
try:
|
|
35
|
+
logger.info(f"Transcribing audio file: {file_path} using Azure Speech-to-Text API")
|
|
36
|
+
text = await self.azure_services.recognize_azure_speech_to_text_from_file(file_path)
|
|
37
|
+
return text if text else "# Audio File\n\n(Transcription not available.)"
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"Error transcribing audio file with Azure Speech-to-Text API: {e}")
|
|
40
|
+
return "# Audio File\n\n(Transcription failed.)"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseHandler:
|
|
5
|
+
extensions: set = frozenset()
|
|
6
|
+
pipeline: list = None
|
|
7
|
+
|
|
8
|
+
def __init__(self, *args, **kwargs):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
async def is_valid(cls, file_path: str) -> bool:
|
|
13
|
+
return Path(file_path).is_file() and Path(file_path).suffix in cls.extensions
|
|
14
|
+
|
|
15
|
+
async def handle(self, file_path, *args, **kwargs) -> str:
|
|
16
|
+
raise NotImplementedError("You must implement the handle method")
|