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.
Files changed (33) hide show
  1. markitdown_pro/__init__.py +1 -0
  2. markitdown_pro/common/__init__.py +0 -0
  3. markitdown_pro/common/logger.py +6 -0
  4. markitdown_pro/common/utils.py +59 -0
  5. markitdown_pro/conversion_pipeline.py +212 -0
  6. markitdown_pro/converters/__init__.py +0 -0
  7. markitdown_pro/converters/azure_docint.py +13 -0
  8. markitdown_pro/converters/base.py +26 -0
  9. markitdown_pro/converters/gpt4o_mini_vision.py +21 -0
  10. markitdown_pro/converters/markitdown_wrapper.py +44 -0
  11. markitdown_pro/converters/pymupdf_wrapper.py +42 -0
  12. markitdown_pro/converters/unstructured_wrapper.py +62 -0
  13. markitdown_pro/converters/youtube_wrapper.py +67 -0
  14. markitdown_pro/handlers/__init__.py +0 -0
  15. markitdown_pro/handlers/audio_handler.py +40 -0
  16. markitdown_pro/handlers/base_handler.py +16 -0
  17. markitdown_pro/handlers/email_handler.py +169 -0
  18. markitdown_pro/handlers/epub_handler.py +33 -0
  19. markitdown_pro/handlers/image_handler.py +48 -0
  20. markitdown_pro/handlers/ipynb_handler.py +31 -0
  21. markitdown_pro/handlers/markup_handler.py +75 -0
  22. markitdown_pro/handlers/office_handler.py +47 -0
  23. markitdown_pro/handlers/pdf_handler.py +122 -0
  24. markitdown_pro/handlers/pst_handler.py +153 -0
  25. markitdown_pro/handlers/tabular_handler.py +34 -0
  26. markitdown_pro/handlers/text_handler.py +38 -0
  27. markitdown_pro/services/__init__.py +0 -0
  28. markitdown_pro/services/azure_service.py +160 -0
  29. markitdown_pro/services/openai_services.py +209 -0
  30. markitdown_pro-0.1.0.dist-info/METADATA +367 -0
  31. markitdown_pro-0.1.0.dist-info/RECORD +33 -0
  32. markitdown_pro-0.1.0.dist-info/WHEEL +5 -0
  33. 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,6 @@
1
+ import logging
2
+ import os
3
+
4
+ log_level = int(os.getenv("LOG_LEVEL", "20"))
5
+ logger = logging.getLogger(__name__)
6
+ logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] %(message)s")
@@ -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")