mdengine 0.3.0__tar.gz → 0.4.0__tar.gz

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 (180) hide show
  1. {mdengine-0.3.0 → mdengine-0.4.0}/PKG-INFO +54 -7
  2. {mdengine-0.3.0 → mdengine-0.4.0}/README.md +34 -5
  3. {mdengine-0.3.0 → mdengine-0.4.0}/pyproject.toml +31 -2
  4. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/__init__.py +1 -1
  5. mdengine-0.4.0/src/md_generator/media/youtube/__init__.py +34 -0
  6. mdengine-0.4.0/src/md_generator/media/youtube/api/__init__.py +1 -0
  7. mdengine-0.4.0/src/md_generator/media/youtube/api/main.py +140 -0
  8. mdengine-0.4.0/src/md_generator/media/youtube/api/mcp_server.py +31 -0
  9. mdengine-0.4.0/src/md_generator/media/youtube/api/mcp_setup.py +48 -0
  10. mdengine-0.4.0/src/md_generator/media/youtube/api/run.py +30 -0
  11. mdengine-0.4.0/src/md_generator/media/youtube/api/settings.py +24 -0
  12. mdengine-0.4.0/src/md_generator/media/youtube/converter.py +140 -0
  13. mdengine-0.4.0/src/md_generator/media/youtube/formatter.py +58 -0
  14. mdengine-0.4.0/src/md_generator/media/youtube/metadata.py +210 -0
  15. mdengine-0.4.0/src/md_generator/media/youtube/service.py +200 -0
  16. mdengine-0.4.0/src/md_generator/media/youtube/transcript.py +100 -0
  17. mdengine-0.4.0/src/md_generator/playwright/__init__.py +6 -0
  18. mdengine-0.4.0/src/md_generator/playwright/api/__init__.py +1 -0
  19. mdengine-0.4.0/src/md_generator/playwright/api/convert_runner.py +71 -0
  20. mdengine-0.4.0/src/md_generator/playwright/api/jobs.py +92 -0
  21. mdengine-0.4.0/src/md_generator/playwright/api/main.py +295 -0
  22. mdengine-0.4.0/src/md_generator/playwright/api/mcp_server.py +37 -0
  23. mdengine-0.4.0/src/md_generator/playwright/api/mcp_setup.py +58 -0
  24. mdengine-0.4.0/src/md_generator/playwright/api/run.py +29 -0
  25. mdengine-0.4.0/src/md_generator/playwright/api/settings.py +26 -0
  26. mdengine-0.4.0/src/md_generator/playwright/assets.py +119 -0
  27. mdengine-0.4.0/src/md_generator/playwright/chunker.py +53 -0
  28. mdengine-0.4.0/src/md_generator/playwright/cli.py +131 -0
  29. mdengine-0.4.0/src/md_generator/playwright/extractor.py +24 -0
  30. mdengine-0.4.0/src/md_generator/playwright/html_to_md.py +25 -0
  31. mdengine-0.4.0/src/md_generator/playwright/options.py +35 -0
  32. mdengine-0.4.0/src/md_generator/playwright/pipeline.py +65 -0
  33. mdengine-0.4.0/src/md_generator/playwright/playwright_fetcher.py +107 -0
  34. {mdengine-0.3.0 → mdengine-0.4.0}/src/mdengine.egg-info/PKG-INFO +54 -7
  35. {mdengine-0.3.0 → mdengine-0.4.0}/src/mdengine.egg-info/SOURCES.txt +29 -0
  36. {mdengine-0.3.0 → mdengine-0.4.0}/src/mdengine.egg-info/entry_points.txt +6 -0
  37. {mdengine-0.3.0 → mdengine-0.4.0}/src/mdengine.egg-info/requires.txt +20 -0
  38. {mdengine-0.3.0 → mdengine-0.4.0}/LICENSE +0 -0
  39. {mdengine-0.3.0 → mdengine-0.4.0}/setup.cfg +0 -0
  40. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/__init__.py +0 -0
  41. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/__init__.py +0 -0
  42. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/convert_runner.py +0 -0
  43. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/jobs.py +0 -0
  44. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/main.py +0 -0
  45. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/mcp_server.py +0 -0
  46. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/mcp_setup.py +0 -0
  47. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/query_options.py +0 -0
  48. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/api/settings.py +0 -0
  49. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/convert_impl.py +0 -0
  50. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/converter.py +0 -0
  51. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/archive/options.py +0 -0
  52. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/__init__.py +0 -0
  53. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/api/__init__.py +0 -0
  54. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/api/main.py +0 -0
  55. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/api/mcp_server.py +0 -0
  56. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/api/query_options.py +0 -0
  57. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/api/settings.py +0 -0
  58. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/api/staging.py +0 -0
  59. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/api/zip_bundle.py +0 -0
  60. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/backends/__init__.py +0 -0
  61. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/backends/base.py +0 -0
  62. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/backends/easy.py +0 -0
  63. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/backends/paddle.py +0 -0
  64. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/backends/tesseract.py +0 -0
  65. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/convert_impl.py +0 -0
  66. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/converter.py +0 -0
  67. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/emit.py +0 -0
  68. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/io_util.py +0 -0
  69. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/image/utils.py +0 -0
  70. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/__init__.py +0 -0
  71. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/__init__.py +0 -0
  72. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/api/__init__.py +0 -0
  73. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/api/main.py +0 -0
  74. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/api/mcp_server.py +0 -0
  75. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/api/mcp_setup.py +0 -0
  76. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/api/run.py +0 -0
  77. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/api/settings.py +0 -0
  78. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/converter.py +0 -0
  79. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/formatter.py +0 -0
  80. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/audio/service.py +0 -0
  81. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/document_converter.py +0 -0
  82. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/job_store.py +0 -0
  83. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/__init__.py +0 -0
  84. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/api/__init__.py +0 -0
  85. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/api/main.py +0 -0
  86. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/api/mcp_server.py +0 -0
  87. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/api/mcp_setup.py +0 -0
  88. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/api/run.py +0 -0
  89. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/api/settings.py +0 -0
  90. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/converter.py +0 -0
  91. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/formatter.py +0 -0
  92. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/video/service.py +0 -0
  93. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/media/whisper_language.py +0 -0
  94. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/__init__.py +0 -0
  95. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/api/__init__.py +0 -0
  96. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/api/main.py +0 -0
  97. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/api/mcp_server.py +0 -0
  98. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/api/settings.py +0 -0
  99. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/api/zip_bundle.py +0 -0
  100. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/converter.py +0 -0
  101. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/md_emit.py +0 -0
  102. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/pdf_extract.py +0 -0
  103. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/pdf/utils.py +0 -0
  104. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/__init__.py +0 -0
  105. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/__init__.py +0 -0
  106. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/convert_runner.py +0 -0
  107. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/jobs.py +0 -0
  108. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/main.py +0 -0
  109. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/mcp_server.py +0 -0
  110. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/mcp_setup.py +0 -0
  111. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/query_options.py +0 -0
  112. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/api/settings.py +0 -0
  113. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/convert_impl.py +0 -0
  114. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/converter.py +0 -0
  115. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/embedded_extract.py +0 -0
  116. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/ooxml_media.py +0 -0
  117. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/options.py +0 -0
  118. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/post_assets.py +0 -0
  119. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/text_formatting.py +0 -0
  120. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/vendor_pdf_md/__init__.py +0 -0
  121. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/vendor_pdf_md/convert.py +0 -0
  122. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/vendor_word_md/__init__.py +0 -0
  123. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/vendor_word_md/convert.py +0 -0
  124. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/ppt/zip_deep.py +0 -0
  125. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/__init__.py +0 -0
  126. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/__init__.py +0 -0
  127. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/convert_runner.py +0 -0
  128. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/jobs.py +0 -0
  129. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/main.py +0 -0
  130. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/mcp_server.py +0 -0
  131. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/mcp_setup.py +0 -0
  132. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/query_options.py +0 -0
  133. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/api/settings.py +0 -0
  134. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/convert_impl.py +0 -0
  135. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/converter.py +0 -0
  136. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/format_detect.py +0 -0
  137. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/md_emit_json.py +0 -0
  138. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/md_emit_txt.py +0 -0
  139. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/md_emit_xml.py +0 -0
  140. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/text/options.py +0 -0
  141. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/__init__.py +0 -0
  142. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/__init__.py +0 -0
  143. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/convert_runner.py +0 -0
  144. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/jobs.py +0 -0
  145. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/main.py +0 -0
  146. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/mcp_server.py +0 -0
  147. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/mcp_setup.py +0 -0
  148. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/query_options.py +0 -0
  149. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/api/settings.py +0 -0
  150. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/assets.py +0 -0
  151. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/convert_impl.py +0 -0
  152. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/converter.py +0 -0
  153. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/crawl.py +0 -0
  154. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/extract.py +0 -0
  155. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/fetch.py +0 -0
  156. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/html_to_md.py +0 -0
  157. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/options.py +0 -0
  158. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/page_convert.py +0 -0
  159. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/post_convert_assets.py +0 -0
  160. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/url/robots_util.py +0 -0
  161. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/__init__.py +0 -0
  162. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/api/__init__.py +0 -0
  163. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/api/convert_util.py +0 -0
  164. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/api/jobs.py +0 -0
  165. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/api/main.py +0 -0
  166. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/api/mcp_server.py +0 -0
  167. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/artifact.py +0 -0
  168. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/converter.py +0 -0
  169. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/word/settings.py +0 -0
  170. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/__init__.py +0 -0
  171. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/api/__init__.py +0 -0
  172. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/api/app.py +0 -0
  173. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/convert_config.py +0 -0
  174. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/converter.py +0 -0
  175. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/converter_core.py +0 -0
  176. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/excel_reader.py +0 -0
  177. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/markdown_emitter.py +0 -0
  178. {mdengine-0.3.0 → mdengine-0.4.0}/src/md_generator/xlsx/mcp_server.py +0 -0
  179. {mdengine-0.3.0 → mdengine-0.4.0}/src/mdengine.egg-info/dependency_links.txt +0 -0
  180. {mdengine-0.3.0 → mdengine-0.4.0}/src/mdengine.egg-info/top_level.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdengine
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Convert PDF, Office, images, audio, video, text/JSON/XML, ZIP archives, and web URLs to Markdown.
5
5
  Author: mdengine contributors
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/vishal7090/md-generator
8
8
  Project-URL: Repository, https://github.com/vishal7090/md-generator
9
9
  Project-URL: Issues, https://github.com/vishal7090/md-generator/issues
10
- Keywords: markdown,pdf,docx,pptx,xlsx,ocr,zip,url,html
10
+ Keywords: markdown,pdf,docx,pptx,xlsx,ocr,zip,url,html,youtube,playwright
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Classifier: Topic :: Text Processing :: Markup :: Markdown
@@ -74,6 +74,19 @@ Requires-Dist: imageio-ffmpeg>=0.5.1; extra == "audio"
74
74
  Provides-Extra: video
75
75
  Requires-Dist: openai-whisper>=20231117; extra == "video"
76
76
  Requires-Dist: imageio-ffmpeg>=0.5.1; extra == "video"
77
+ Provides-Extra: youtube
78
+ Requires-Dist: youtube-transcript-api>=0.6.0; extra == "youtube"
79
+ Requires-Dist: httpx>=0.27.0; extra == "youtube"
80
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "youtube"
81
+ Requires-Dist: lxml>=5.0.0; extra == "youtube"
82
+ Provides-Extra: playwright
83
+ Requires-Dist: playwright>=1.49.0; extra == "playwright"
84
+ Requires-Dist: httpx>=0.27.0; extra == "playwright"
85
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "playwright"
86
+ Requires-Dist: lxml>=5.0.0; extra == "playwright"
87
+ Requires-Dist: markdownify>=0.14.0; extra == "playwright"
88
+ Requires-Dist: readability-lxml>=0.8.1; extra == "playwright"
89
+ Requires-Dist: Pillow>=10.0.0; extra == "playwright"
77
90
  Provides-Extra: api
78
91
  Requires-Dist: fastapi>=0.115.0; extra == "api"
79
92
  Requires-Dist: uvicorn[standard]>=0.32.0; extra == "api"
@@ -86,9 +99,13 @@ Requires-Dist: fastmcp>=2.3.0; extra == "mcp"
86
99
  Provides-Extra: dev
87
100
  Requires-Dist: pytest>=8.0.0; extra == "dev"
88
101
  Requires-Dist: httpx>=0.27.0; extra == "dev"
102
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "dev"
103
+ Requires-Dist: lxml>=5.0.0; extra == "dev"
104
+ Requires-Dist: youtube-transcript-api>=0.6.0; extra == "dev"
89
105
  Requires-Dist: mdengine[api]; extra == "dev"
90
106
  Requires-Dist: mdengine[mcp]; extra == "dev"
91
107
  Requires-Dist: mdengine[url]; extra == "dev"
108
+ Requires-Dist: mdengine[playwright]; extra == "dev"
92
109
  Provides-Extra: all
93
110
  Requires-Dist: pymupdf>=1.24.0; extra == "all"
94
111
  Requires-Dist: pdfplumber>=0.11.0; extra == "all"
@@ -114,6 +131,7 @@ Requires-Dist: fastmcp>=2.3.0; extra == "all"
114
131
  Requires-Dist: beautifulsoup4>=4.12.0; extra == "all"
115
132
  Requires-Dist: lxml_html_clean>=0.4.0; extra == "all"
116
133
  Requires-Dist: readability-lxml>=0.8.1; extra == "all"
134
+ Requires-Dist: youtube-transcript-api>=0.6.0; extra == "all"
117
135
  Dynamic: license-file
118
136
 
119
137
  # mdengine
@@ -238,10 +256,13 @@ If the shell reports “command not found”, ensure the Python **Scripts** dire
238
256
  | `md-url` | `md_generator.url.converter:main` | `md-url https://example.com/doc ./web-out --artifact-layout` |
239
257
  | `md-audio` | `md_generator.media.audio.converter:main` | `md-audio clip.mp3 transcript.md --model base` |
240
258
  | `md-video` | `md_generator.media.video.converter:main` | `md-video clip.mp4 transcript.md --model base` |
259
+ | `md-youtube` | `md_generator.media.youtube.converter:main` | `md-youtube "https://youtu.be/…" out.md --transcript-lang en` |
241
260
  | `md-audio-api` | `md_generator.media.audio.api.run:main` | REST + MCP on port **8011** (see [Audio and video to Markdown](#audio-and-video-to-markdown)) |
242
261
  | `md-video-api` | `md_generator.media.video.api.run:main` | REST + MCP on port **8012** |
262
+ | `md-youtube-api` | `md_generator.media.youtube.api.run:main` | REST + MCP on port **8013** (JSON `url` body; see same section) |
243
263
  | `md-audio-mcp` | `md_generator.media.audio.api.mcp_server:main` | Standalone MCP (`--transport stdio` \| `sse` \| `streamable-http`) |
244
264
  | `md-video-mcp` | `md_generator.media.video.api.mcp_server:main` | Same for video |
265
+ | `md-youtube-mcp` | `md_generator.media.youtube.api.mcp_server:main` | Same for YouTube (`youtube_url_to_markdown`) |
245
266
 
246
267
  Every command accepts **`-h` / `--help`** for full flags (artifact layout, OCR, ZIP options, etc.).
247
268
 
@@ -455,6 +476,7 @@ Library code lives under **`md_generator.media`**: shared probing in [`document_
455
476
  ```bash
456
477
  pip install "mdengine[audio,api,mcp]" # audio CLI + HTTP + MCP
457
478
  pip install "mdengine[video,api,mcp]" # video CLI + HTTP + MCP (same ML stack as audio)
479
+ pip install "mdengine[youtube,api,mcp]" # YouTube URL → Markdown (captions + metadata; optional Whisper fallback)
458
480
  ```
459
481
 
460
482
  ### Python library
@@ -483,6 +505,18 @@ md = svc.to_markdown(Path("input.mp4"), title=None)
483
505
  svc.write_markdown(Path("input.mp4"), Path("out/transcript.md"))
484
506
  ```
485
507
 
508
+ **YouTube** — captions API + page metadata (BeautifulSoup / oEmbed); optional **yt-dlp + Whisper** when captions are missing (`mdengine[audio]` and `yt-dlp` on `PATH`, or `MD_YOUTUBE_YTDLP`):
509
+
510
+ ```python
511
+ from md_generator.media.youtube import YouTubeToMarkdownService
512
+
513
+ svc = YouTubeToMarkdownService(whisper_model="base")
514
+ md = svc.to_markdown("https://www.youtube.com/watch?v=VIDEO_ID", transcript_languages=["en"])
515
+ svc.write_markdown("https://youtu.be/VIDEO_ID", Path("out/youtube.md"))
516
+ ```
517
+
518
+ For file-based pipelines, [`YouTubeConverter`](src/md_generator/media/youtube/converter.py) reads a `.url` / `.yturl` / `.youtube` file (or a `.txt` whose first non-comment line is a YouTube URL) and implements [`DocumentConverter`](src/md_generator/media/document_converter.py).
519
+
486
520
  Public symbols are also re-exported from [`md_generator.media`](src/md_generator/media/__init__.py) for ffprobe helpers (`ffprobe_json`, `VideoProbeResult`, …).
487
521
 
488
522
  ### REST API (FastAPI)
@@ -497,13 +531,15 @@ Each service exposes the same job pattern as other converters:
497
531
  | `GET /convert/jobs/{job_id}/download` | Markdown when `done`; workspace removed after download. |
498
532
 
499
533
  **Audio** defaults: `MD_AUDIO_MAX_UPLOAD_MB=200`, `MD_AUDIO_MAX_SYNC_UPLOAD_MB=40`, `MD_AUDIO_API_PORT=8011`.
500
- **Video** defaults: `MD_VIDEO_MAX_UPLOAD_MB=500`, `MD_VIDEO_MAX_SYNC_UPLOAD_MB=80`, `MD_VIDEO_API_PORT=8012`.
534
+ **Video** defaults: `MD_VIDEO_MAX_UPLOAD_MB=500`, `MD_VIDEO_MAX_SYNC_UPLOAD_MB=80`, `MD_VIDEO_API_PORT=8012`.
535
+ **YouTube** uses **JSON** (not multipart): `POST /convert/sync` and `POST /convert/jobs` accept `{"url":"https://www.youtube.com/watch?v=…","title":null,"transcript_languages":["en"],"enable_audio_fallback":true,"whisper_model":"base","language":null}`. Defaults: `MD_YOUTUBE_API_PORT=8013`, `MD_YOUTUBE_JOB_TTL_SECONDS`, `MD_YOUTUBE_CORS_ORIGINS`, `MD_YOUTUBE_TEMP_DIR`.
501
536
 
502
537
  Run with the bundled runners (each call builds the app with **`factory=True`** for a clean MCP session manager):
503
538
 
504
539
  ```bash
505
540
  md-audio-api --host 127.0.0.1 --port 8011
506
541
  md-video-api --host 127.0.0.1 --port 8012
542
+ md-youtube-api --host 127.0.0.1 --port 8013
507
543
  ```
508
544
 
509
545
  Or with Uvicorn directly (the ASGI app is built by **`create_app()`** so each worker gets its own MCP session manager):
@@ -511,6 +547,7 @@ Or with Uvicorn directly (the ASGI app is built by **`create_app()`** so each wo
511
547
  ```bash
512
548
  uvicorn md_generator.media.audio.api.main:create_app --factory --host 127.0.0.1 --port 8011
513
549
  uvicorn md_generator.media.video.api.main:create_app --factory --host 127.0.0.1 --port 8012
550
+ uvicorn md_generator.media.youtube.api.main:create_app --factory --host 127.0.0.1 --port 8013
514
551
  ```
515
552
 
516
553
  The module also defines **`app = create_app()`** for a single-process target: `uvicorn md_generator.media.audio.api.main:app` (no `--factory`).
@@ -519,7 +556,7 @@ Swagger is at **`/docs`** when the app is running.
519
556
 
520
557
  ### MCP (stdio, SSE, streamable HTTP)
521
558
 
522
- 1. **With FastAPI** — start `md-audio-api` or `md-video-api`; mount Streamable HTTP MCP at **`http://<host>:<port>/mcp`** (same host as REST).
559
+ 1. **With FastAPI** — start `md-audio-api`, `md-video-api`, or `md-youtube-api`; mount Streamable HTTP MCP at **`http://<host>:<port>/mcp`** (same host as REST).
523
560
  2. **Standalone** — process speaks MCP only:
524
561
 
525
562
  ```bash
@@ -527,16 +564,18 @@ md-audio-mcp --transport stdio
527
564
  md-audio-mcp --transport sse
528
565
  md-audio-mcp --transport streamable-http
529
566
  md-video-mcp --transport stdio
567
+ md-youtube-mcp --transport stdio
530
568
  ```
531
569
 
532
570
  **Audio MCP tools:** `transcribe_audio_path`, `transcribe_audio_base64`.
533
- **Video MCP tools:** `transcribe_video_path`, `transcribe_video_base64`.
571
+ **Video MCP tools:** `transcribe_video_path`, `transcribe_video_base64`.
572
+ **YouTube MCP tool:** `youtube_url_to_markdown`.
534
573
 
535
- Equivalent modules: `python -m md_generator.media.audio.api.mcp_server`, `python -m md_generator.media.video.api.mcp_server`.
574
+ Equivalent modules: `python -m md_generator.media.audio.api.mcp_server`, `python -m md_generator.media.video.api.mcp_server`, `python -m md_generator.media.youtube.api.mcp_server`.
536
575
 
537
576
  ### Thin shims (repo clone)
538
577
 
539
- [`audio-to-md/converter.py`](audio-to-md/converter.py) and [`video-to-md/converter.py`](video-to-md/converter.py) delegate to the same `main` as `md-audio` / `md-video`. Tests and `pytest.ini` live under `audio-to-md/tests/` and `video-to-md/tests/`.
578
+ [`audio-to-md/converter.py`](audio-to-md/converter.py), [`video-to-md/converter.py`](video-to-md/converter.py), and [`youtube-to-md/converter.py`](youtube-to-md/converter.py) delegate to the same `main` as `md-audio` / `md-video` / `md-youtube`. Tests and `pytest.ini` live under `audio-to-md/tests/`, `video-to-md/tests/`, and `youtube-to-md/tests/`.
540
579
 
541
580
  ---
542
581
 
@@ -565,8 +604,10 @@ Install `mdengine[api]` plus the format extra(s), then run the **`app`** object
565
604
  | Text/JSON/XML | `md_generator.text.api.main:app` | `text`, `api`, `mcp` |
566
605
  | ZIP | `md_generator.archive.api.main:app` | `archive`, `api`, `mcp` (+ extras for nested office/PDF) |
567
606
  | URL / HTML | `md_generator.url.api.main:app` | `url`, `api`, `mcp` |
607
+ | Playwright / SPA | `md_generator.playwright.api.main:app` | `playwright`, `api`, `mcp` |
568
608
  | Audio (Whisper) | `md_generator.media.audio.api.main:create_app` (use **`--factory`**) or `…main:app` | `audio`, `api`, `mcp` |
569
609
  | Video (Whisper) | `md_generator.media.video.api.main:create_app` (use **`--factory`**) or `…main:app` | `video`, `api`, `mcp` |
610
+ | YouTube | `md_generator.media.youtube.api.main:create_app` (use **`--factory`**) or `…main:app` | `youtube`, `api`, `mcp` |
570
611
 
571
612
  Examples:
572
613
 
@@ -575,8 +616,10 @@ uvicorn md_generator.pdf.api.main:app --host 127.0.0.1 --port 8001
575
616
  uvicorn md_generator.word.api.main:app --host 127.0.0.1 --port 8002
576
617
  uvicorn md_generator.archive.api.main:app --host 127.0.0.1 --port 8010
577
618
  uvicorn md_generator.url.api.main:app --host 127.0.0.1 --port 8011
619
+ uvicorn md_generator.playwright.api.main:app --host 127.0.0.1 --port 8014
578
620
  uvicorn md_generator.media.audio.api.main:create_app --factory --host 127.0.0.1 --port 8011
579
621
  uvicorn md_generator.media.video.api.main:create_app --factory --host 127.0.0.1 --port 8012
622
+ uvicorn md_generator.media.youtube.api.main:create_app --factory --host 127.0.0.1 --port 8013
580
623
  ```
581
624
 
582
625
  ### MCP over HTTP on the same server
@@ -596,8 +639,10 @@ Prefixes differ per service (often read from a `.env` file next to the process):
596
639
  | Text | `TXT_JSON_XML_TO_MD_` | same pattern |
597
640
  | XLSX | `XLSX_TO_MD_` | `XLSX_TO_MD_TEMP_DIR`, `XLSX_TO_MD_CORS_ORIGINS`, etc. (see `md_generator.xlsx.api.app`) |
598
641
  | URL | `URL_TO_MD_` | `URL_TO_MD_MAX_SYNC_URLS`, `URL_TO_MD_MAX_SYNC_CRAWL_PAGES`, `URL_TO_MD_MAX_JOB_URLS`, `URL_TO_MD_JOB_TTL_SECONDS`, `URL_TO_MD_TEMP_DIR`, `URL_TO_MD_CORS_ORIGINS` |
642
+ | Playwright / SPA | `PLAYWRIGHT_TO_MD_` | `PLAYWRIGHT_TO_MD_MAX_SYNC_URLS`, `PLAYWRIGHT_TO_MD_MAX_JOB_URLS`, `PLAYWRIGHT_TO_MD_JOB_TTL_SECONDS`, `PLAYWRIGHT_TO_MD_TEMP_DIR`, `PLAYWRIGHT_TO_MD_CORS_ORIGINS`, `PLAYWRIGHT_TO_MD_API_HOST`, `PLAYWRIGHT_TO_MD_API_PORT` (default **8014**) |
599
643
  | Audio API | `MD_AUDIO_` | `MD_AUDIO_MAX_UPLOAD_MB`, `MD_AUDIO_MAX_SYNC_UPLOAD_MB`, `MD_AUDIO_JOB_TTL_SECONDS`, `MD_AUDIO_TEMP_DIR`, `MD_AUDIO_CORS_ORIGINS`, `MD_AUDIO_API_HOST`, `MD_AUDIO_API_PORT` |
600
644
  | Video API | `MD_VIDEO_` | Same pattern as audio with `MD_VIDEO_*` (defaults: larger upload/sync caps, port **8012**) |
645
+ | YouTube API | `MD_YOUTUBE_` | `MD_YOUTUBE_JOB_TTL_SECONDS`, `MD_YOUTUBE_TEMP_DIR`, `MD_YOUTUBE_CORS_ORIGINS`, `MD_YOUTUBE_API_HOST`, `MD_YOUTUBE_API_PORT` (default **8013**); optional `MD_YOUTUBE_YTDLP` path for audio fallback |
601
646
 
602
647
  Exact variable names match the `ApiSettings` / helper functions in each `api/settings` or `api/app` module.
603
648
 
@@ -621,8 +666,10 @@ Two usage patterns:
621
666
  | PPTX | `python -m md_generator.ppt.api.mcp_server` (see module docstring for flags) |
622
667
  | Image | `python -m md_generator.image.api.mcp_server` (see module for CLI) |
623
668
  | URL / HTML | `python -m md_generator.url.api.mcp_server` / `--transport sse` / `--transport streamable-http` |
669
+ | Playwright / SPA | `md-playwright-mcp` or `python -m md_generator.playwright.api.mcp_server` / `--transport sse` / `--transport streamable-http` |
624
670
  | Audio | `md-audio-mcp` or `python -m md_generator.media.audio.api.mcp_server` — `--transport stdio` (default), `sse`, `streamable-http` |
625
671
  | Video | `md-video-mcp` or `python -m md_generator.media.video.api.mcp_server` — same transports |
672
+ | YouTube | `md-youtube-mcp` or `python -m md_generator.media.youtube.api.mcp_server` — same transports |
626
673
 
627
674
  **Word** and **XLSX** also ship a small runner script in the repo:
628
675
 
@@ -120,10 +120,13 @@ If the shell reports “command not found”, ensure the Python **Scripts** dire
120
120
  | `md-url` | `md_generator.url.converter:main` | `md-url https://example.com/doc ./web-out --artifact-layout` |
121
121
  | `md-audio` | `md_generator.media.audio.converter:main` | `md-audio clip.mp3 transcript.md --model base` |
122
122
  | `md-video` | `md_generator.media.video.converter:main` | `md-video clip.mp4 transcript.md --model base` |
123
+ | `md-youtube` | `md_generator.media.youtube.converter:main` | `md-youtube "https://youtu.be/…" out.md --transcript-lang en` |
123
124
  | `md-audio-api` | `md_generator.media.audio.api.run:main` | REST + MCP on port **8011** (see [Audio and video to Markdown](#audio-and-video-to-markdown)) |
124
125
  | `md-video-api` | `md_generator.media.video.api.run:main` | REST + MCP on port **8012** |
126
+ | `md-youtube-api` | `md_generator.media.youtube.api.run:main` | REST + MCP on port **8013** (JSON `url` body; see same section) |
125
127
  | `md-audio-mcp` | `md_generator.media.audio.api.mcp_server:main` | Standalone MCP (`--transport stdio` \| `sse` \| `streamable-http`) |
126
128
  | `md-video-mcp` | `md_generator.media.video.api.mcp_server:main` | Same for video |
129
+ | `md-youtube-mcp` | `md_generator.media.youtube.api.mcp_server:main` | Same for YouTube (`youtube_url_to_markdown`) |
127
130
 
128
131
  Every command accepts **`-h` / `--help`** for full flags (artifact layout, OCR, ZIP options, etc.).
129
132
 
@@ -337,6 +340,7 @@ Library code lives under **`md_generator.media`**: shared probing in [`document_
337
340
  ```bash
338
341
  pip install "mdengine[audio,api,mcp]" # audio CLI + HTTP + MCP
339
342
  pip install "mdengine[video,api,mcp]" # video CLI + HTTP + MCP (same ML stack as audio)
343
+ pip install "mdengine[youtube,api,mcp]" # YouTube URL → Markdown (captions + metadata; optional Whisper fallback)
340
344
  ```
341
345
 
342
346
  ### Python library
@@ -365,6 +369,18 @@ md = svc.to_markdown(Path("input.mp4"), title=None)
365
369
  svc.write_markdown(Path("input.mp4"), Path("out/transcript.md"))
366
370
  ```
367
371
 
372
+ **YouTube** — captions API + page metadata (BeautifulSoup / oEmbed); optional **yt-dlp + Whisper** when captions are missing (`mdengine[audio]` and `yt-dlp` on `PATH`, or `MD_YOUTUBE_YTDLP`):
373
+
374
+ ```python
375
+ from md_generator.media.youtube import YouTubeToMarkdownService
376
+
377
+ svc = YouTubeToMarkdownService(whisper_model="base")
378
+ md = svc.to_markdown("https://www.youtube.com/watch?v=VIDEO_ID", transcript_languages=["en"])
379
+ svc.write_markdown("https://youtu.be/VIDEO_ID", Path("out/youtube.md"))
380
+ ```
381
+
382
+ For file-based pipelines, [`YouTubeConverter`](src/md_generator/media/youtube/converter.py) reads a `.url` / `.yturl` / `.youtube` file (or a `.txt` whose first non-comment line is a YouTube URL) and implements [`DocumentConverter`](src/md_generator/media/document_converter.py).
383
+
368
384
  Public symbols are also re-exported from [`md_generator.media`](src/md_generator/media/__init__.py) for ffprobe helpers (`ffprobe_json`, `VideoProbeResult`, …).
369
385
 
370
386
  ### REST API (FastAPI)
@@ -379,13 +395,15 @@ Each service exposes the same job pattern as other converters:
379
395
  | `GET /convert/jobs/{job_id}/download` | Markdown when `done`; workspace removed after download. |
380
396
 
381
397
  **Audio** defaults: `MD_AUDIO_MAX_UPLOAD_MB=200`, `MD_AUDIO_MAX_SYNC_UPLOAD_MB=40`, `MD_AUDIO_API_PORT=8011`.
382
- **Video** defaults: `MD_VIDEO_MAX_UPLOAD_MB=500`, `MD_VIDEO_MAX_SYNC_UPLOAD_MB=80`, `MD_VIDEO_API_PORT=8012`.
398
+ **Video** defaults: `MD_VIDEO_MAX_UPLOAD_MB=500`, `MD_VIDEO_MAX_SYNC_UPLOAD_MB=80`, `MD_VIDEO_API_PORT=8012`.
399
+ **YouTube** uses **JSON** (not multipart): `POST /convert/sync` and `POST /convert/jobs` accept `{"url":"https://www.youtube.com/watch?v=…","title":null,"transcript_languages":["en"],"enable_audio_fallback":true,"whisper_model":"base","language":null}`. Defaults: `MD_YOUTUBE_API_PORT=8013`, `MD_YOUTUBE_JOB_TTL_SECONDS`, `MD_YOUTUBE_CORS_ORIGINS`, `MD_YOUTUBE_TEMP_DIR`.
383
400
 
384
401
  Run with the bundled runners (each call builds the app with **`factory=True`** for a clean MCP session manager):
385
402
 
386
403
  ```bash
387
404
  md-audio-api --host 127.0.0.1 --port 8011
388
405
  md-video-api --host 127.0.0.1 --port 8012
406
+ md-youtube-api --host 127.0.0.1 --port 8013
389
407
  ```
390
408
 
391
409
  Or with Uvicorn directly (the ASGI app is built by **`create_app()`** so each worker gets its own MCP session manager):
@@ -393,6 +411,7 @@ Or with Uvicorn directly (the ASGI app is built by **`create_app()`** so each wo
393
411
  ```bash
394
412
  uvicorn md_generator.media.audio.api.main:create_app --factory --host 127.0.0.1 --port 8011
395
413
  uvicorn md_generator.media.video.api.main:create_app --factory --host 127.0.0.1 --port 8012
414
+ uvicorn md_generator.media.youtube.api.main:create_app --factory --host 127.0.0.1 --port 8013
396
415
  ```
397
416
 
398
417
  The module also defines **`app = create_app()`** for a single-process target: `uvicorn md_generator.media.audio.api.main:app` (no `--factory`).
@@ -401,7 +420,7 @@ Swagger is at **`/docs`** when the app is running.
401
420
 
402
421
  ### MCP (stdio, SSE, streamable HTTP)
403
422
 
404
- 1. **With FastAPI** — start `md-audio-api` or `md-video-api`; mount Streamable HTTP MCP at **`http://<host>:<port>/mcp`** (same host as REST).
423
+ 1. **With FastAPI** — start `md-audio-api`, `md-video-api`, or `md-youtube-api`; mount Streamable HTTP MCP at **`http://<host>:<port>/mcp`** (same host as REST).
405
424
  2. **Standalone** — process speaks MCP only:
406
425
 
407
426
  ```bash
@@ -409,16 +428,18 @@ md-audio-mcp --transport stdio
409
428
  md-audio-mcp --transport sse
410
429
  md-audio-mcp --transport streamable-http
411
430
  md-video-mcp --transport stdio
431
+ md-youtube-mcp --transport stdio
412
432
  ```
413
433
 
414
434
  **Audio MCP tools:** `transcribe_audio_path`, `transcribe_audio_base64`.
415
- **Video MCP tools:** `transcribe_video_path`, `transcribe_video_base64`.
435
+ **Video MCP tools:** `transcribe_video_path`, `transcribe_video_base64`.
436
+ **YouTube MCP tool:** `youtube_url_to_markdown`.
416
437
 
417
- Equivalent modules: `python -m md_generator.media.audio.api.mcp_server`, `python -m md_generator.media.video.api.mcp_server`.
438
+ Equivalent modules: `python -m md_generator.media.audio.api.mcp_server`, `python -m md_generator.media.video.api.mcp_server`, `python -m md_generator.media.youtube.api.mcp_server`.
418
439
 
419
440
  ### Thin shims (repo clone)
420
441
 
421
- [`audio-to-md/converter.py`](audio-to-md/converter.py) and [`video-to-md/converter.py`](video-to-md/converter.py) delegate to the same `main` as `md-audio` / `md-video`. Tests and `pytest.ini` live under `audio-to-md/tests/` and `video-to-md/tests/`.
442
+ [`audio-to-md/converter.py`](audio-to-md/converter.py), [`video-to-md/converter.py`](video-to-md/converter.py), and [`youtube-to-md/converter.py`](youtube-to-md/converter.py) delegate to the same `main` as `md-audio` / `md-video` / `md-youtube`. Tests and `pytest.ini` live under `audio-to-md/tests/`, `video-to-md/tests/`, and `youtube-to-md/tests/`.
422
443
 
423
444
  ---
424
445
 
@@ -447,8 +468,10 @@ Install `mdengine[api]` plus the format extra(s), then run the **`app`** object
447
468
  | Text/JSON/XML | `md_generator.text.api.main:app` | `text`, `api`, `mcp` |
448
469
  | ZIP | `md_generator.archive.api.main:app` | `archive`, `api`, `mcp` (+ extras for nested office/PDF) |
449
470
  | URL / HTML | `md_generator.url.api.main:app` | `url`, `api`, `mcp` |
471
+ | Playwright / SPA | `md_generator.playwright.api.main:app` | `playwright`, `api`, `mcp` |
450
472
  | Audio (Whisper) | `md_generator.media.audio.api.main:create_app` (use **`--factory`**) or `…main:app` | `audio`, `api`, `mcp` |
451
473
  | Video (Whisper) | `md_generator.media.video.api.main:create_app` (use **`--factory`**) or `…main:app` | `video`, `api`, `mcp` |
474
+ | YouTube | `md_generator.media.youtube.api.main:create_app` (use **`--factory`**) or `…main:app` | `youtube`, `api`, `mcp` |
452
475
 
453
476
  Examples:
454
477
 
@@ -457,8 +480,10 @@ uvicorn md_generator.pdf.api.main:app --host 127.0.0.1 --port 8001
457
480
  uvicorn md_generator.word.api.main:app --host 127.0.0.1 --port 8002
458
481
  uvicorn md_generator.archive.api.main:app --host 127.0.0.1 --port 8010
459
482
  uvicorn md_generator.url.api.main:app --host 127.0.0.1 --port 8011
483
+ uvicorn md_generator.playwright.api.main:app --host 127.0.0.1 --port 8014
460
484
  uvicorn md_generator.media.audio.api.main:create_app --factory --host 127.0.0.1 --port 8011
461
485
  uvicorn md_generator.media.video.api.main:create_app --factory --host 127.0.0.1 --port 8012
486
+ uvicorn md_generator.media.youtube.api.main:create_app --factory --host 127.0.0.1 --port 8013
462
487
  ```
463
488
 
464
489
  ### MCP over HTTP on the same server
@@ -478,8 +503,10 @@ Prefixes differ per service (often read from a `.env` file next to the process):
478
503
  | Text | `TXT_JSON_XML_TO_MD_` | same pattern |
479
504
  | XLSX | `XLSX_TO_MD_` | `XLSX_TO_MD_TEMP_DIR`, `XLSX_TO_MD_CORS_ORIGINS`, etc. (see `md_generator.xlsx.api.app`) |
480
505
  | URL | `URL_TO_MD_` | `URL_TO_MD_MAX_SYNC_URLS`, `URL_TO_MD_MAX_SYNC_CRAWL_PAGES`, `URL_TO_MD_MAX_JOB_URLS`, `URL_TO_MD_JOB_TTL_SECONDS`, `URL_TO_MD_TEMP_DIR`, `URL_TO_MD_CORS_ORIGINS` |
506
+ | Playwright / SPA | `PLAYWRIGHT_TO_MD_` | `PLAYWRIGHT_TO_MD_MAX_SYNC_URLS`, `PLAYWRIGHT_TO_MD_MAX_JOB_URLS`, `PLAYWRIGHT_TO_MD_JOB_TTL_SECONDS`, `PLAYWRIGHT_TO_MD_TEMP_DIR`, `PLAYWRIGHT_TO_MD_CORS_ORIGINS`, `PLAYWRIGHT_TO_MD_API_HOST`, `PLAYWRIGHT_TO_MD_API_PORT` (default **8014**) |
481
507
  | Audio API | `MD_AUDIO_` | `MD_AUDIO_MAX_UPLOAD_MB`, `MD_AUDIO_MAX_SYNC_UPLOAD_MB`, `MD_AUDIO_JOB_TTL_SECONDS`, `MD_AUDIO_TEMP_DIR`, `MD_AUDIO_CORS_ORIGINS`, `MD_AUDIO_API_HOST`, `MD_AUDIO_API_PORT` |
482
508
  | Video API | `MD_VIDEO_` | Same pattern as audio with `MD_VIDEO_*` (defaults: larger upload/sync caps, port **8012**) |
509
+ | YouTube API | `MD_YOUTUBE_` | `MD_YOUTUBE_JOB_TTL_SECONDS`, `MD_YOUTUBE_TEMP_DIR`, `MD_YOUTUBE_CORS_ORIGINS`, `MD_YOUTUBE_API_HOST`, `MD_YOUTUBE_API_PORT` (default **8013**); optional `MD_YOUTUBE_YTDLP` path for audio fallback |
483
510
 
484
511
  Exact variable names match the `ApiSettings` / helper functions in each `api/settings` or `api/app` module.
485
512
 
@@ -503,8 +530,10 @@ Two usage patterns:
503
530
  | PPTX | `python -m md_generator.ppt.api.mcp_server` (see module docstring for flags) |
504
531
  | Image | `python -m md_generator.image.api.mcp_server` (see module for CLI) |
505
532
  | URL / HTML | `python -m md_generator.url.api.mcp_server` / `--transport sse` / `--transport streamable-http` |
533
+ | Playwright / SPA | `md-playwright-mcp` or `python -m md_generator.playwright.api.mcp_server` / `--transport sse` / `--transport streamable-http` |
506
534
  | Audio | `md-audio-mcp` or `python -m md_generator.media.audio.api.mcp_server` — `--transport stdio` (default), `sse`, `streamable-http` |
507
535
  | Video | `md-video-mcp` or `python -m md_generator.media.video.api.mcp_server` — same transports |
536
+ | YouTube | `md-youtube-mcp` or `python -m md_generator.media.youtube.api.mcp_server` — same transports |
508
537
 
509
538
  **Word** and **XLSX** also ship a small runner script in the repo:
510
539
 
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mdengine"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Convert PDF, Office, images, audio, video, text/JSON/XML, ZIP archives, and web URLs to Markdown."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [{ name = "mdengine contributors" }]
14
- keywords = ["markdown", "pdf", "docx", "pptx", "xlsx", "ocr", "zip", "url", "html"]
14
+ keywords = ["markdown", "pdf", "docx", "pptx", "xlsx", "ocr", "zip", "url", "html", "youtube", "playwright"]
15
15
  classifiers = [
16
16
  "Programming Language :: Python :: 3",
17
17
  "Programming Language :: Python :: 3 :: Only",
@@ -76,6 +76,22 @@ url-full = [
76
76
  audio = ["openai-whisper>=20231117", "imageio-ffmpeg>=0.5.1"]
77
77
  # Same ML stack as audio; ships a bundled ``ffmpeg`` binary when the system has none.
78
78
  video = ["openai-whisper>=20231117", "imageio-ffmpeg>=0.5.1"]
79
+ youtube = [
80
+ "youtube-transcript-api>=0.6.0",
81
+ "httpx>=0.27.0",
82
+ "beautifulsoup4>=4.12.0",
83
+ "lxml>=5.0.0",
84
+ ]
85
+ # Playwright-rendered SPA → Markdown (install browsers: playwright install chromium).
86
+ playwright = [
87
+ "playwright>=1.49.0",
88
+ "httpx>=0.27.0",
89
+ "beautifulsoup4>=4.12.0",
90
+ "lxml>=5.0.0",
91
+ "markdownify>=0.14.0",
92
+ "readability-lxml>=0.8.1",
93
+ "Pillow>=10.0.0",
94
+ ]
79
95
  api = [
80
96
  "fastapi>=0.115.0",
81
97
  "uvicorn[standard]>=0.32.0",
@@ -87,9 +103,13 @@ mcp = ["mcp>=1.2.0", "fastmcp>=2.3.0"]
87
103
  dev = [
88
104
  "pytest>=8.0.0",
89
105
  "httpx>=0.27.0",
106
+ "beautifulsoup4>=4.12.0",
107
+ "lxml>=5.0.0",
108
+ "youtube-transcript-api>=0.6.0",
90
109
  "mdengine[api]",
91
110
  "mdengine[mcp]",
92
111
  "mdengine[url]",
112
+ "mdengine[playwright]",
93
113
  ]
94
114
  all = [
95
115
  "pymupdf>=1.24.0",
@@ -116,6 +136,7 @@ all = [
116
136
  "beautifulsoup4>=4.12.0",
117
137
  "lxml_html_clean>=0.4.0",
118
138
  "readability-lxml>=0.8.1",
139
+ "youtube-transcript-api>=0.6.0",
119
140
  ]
120
141
 
121
142
  [project.urls]
@@ -138,6 +159,12 @@ md-audio-api = "md_generator.media.audio.api.run:main"
138
159
  md-video-api = "md_generator.media.video.api.run:main"
139
160
  md-audio-mcp = "md_generator.media.audio.api.mcp_server:main"
140
161
  md-video-mcp = "md_generator.media.video.api.mcp_server:main"
162
+ md-youtube = "md_generator.media.youtube.converter:main"
163
+ md-youtube-api = "md_generator.media.youtube.api.run:main"
164
+ md-youtube-mcp = "md_generator.media.youtube.api.mcp_server:main"
165
+ md-playwright = "md_generator.playwright.cli:main"
166
+ md-playwright-api = "md_generator.playwright.api.run:main"
167
+ md-playwright-mcp = "md_generator.playwright.api.mcp_server:main"
141
168
 
142
169
  [tool.setuptools.packages.find]
143
170
  where = ["src"]
@@ -155,5 +182,7 @@ testpaths = [
155
182
  "url-to-md/tests",
156
183
  "audio-to-md/tests",
157
184
  "video-to-md/tests",
185
+ "youtube-to-md/tests",
186
+ "playwright-to-md/tests",
158
187
  ]
159
188
  markers = ["integration: optional dependency or slower checks"]
@@ -1,5 +1,5 @@
1
1
  """md_generator: PDF, Office, image, text, and ZIP to Markdown conversion."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -0,0 +1,34 @@
1
+ """YouTube URL → Markdown (metadata, description, timestamped transcript)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from md_generator.media.youtube.converter import YouTubeConverter
6
+ from md_generator.media.youtube.formatter import YouTubeMarkdownFormatter
7
+ from md_generator.media.youtube.metadata import (
8
+ YouTubeMetadataError,
9
+ extract_video_id,
10
+ fetch_youtube_metadata,
11
+ normalize_youtube_url,
12
+ )
13
+ from md_generator.media.youtube.service import (
14
+ YouTubeConversionResult,
15
+ YouTubeError,
16
+ YouTubeToMarkdownService,
17
+ read_youtube_url_from_path,
18
+ )
19
+ from md_generator.media.youtube.transcript import YouTubeTranscriptError, fetch_transcript
20
+
21
+ __all__ = [
22
+ "YouTubeConversionResult",
23
+ "YouTubeConverter",
24
+ "YouTubeError",
25
+ "YouTubeMarkdownFormatter",
26
+ "YouTubeMetadataError",
27
+ "YouTubeToMarkdownService",
28
+ "YouTubeTranscriptError",
29
+ "extract_video_id",
30
+ "fetch_transcript",
31
+ "fetch_youtube_metadata",
32
+ "normalize_youtube_url",
33
+ "read_youtube_url_from_path",
34
+ ]
@@ -0,0 +1 @@
1
+ """YouTube REST API and MCP entrypoints."""
@@ -0,0 +1,140 @@
1
+ """FastAPI: JSON URL conversion (sync + jobs) and Streamable HTTP MCP at ``/mcp``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tempfile
6
+ from contextlib import asynccontextmanager
7
+ from pathlib import Path
8
+
9
+ from fastapi import FastAPI, HTTPException, Request
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import FileResponse, Response
12
+ from pydantic import BaseModel, Field
13
+ from starlette.background import BackgroundTask
14
+
15
+ from md_generator.media.job_store import MediaJobStore
16
+ from md_generator.media.youtube.api.mcp_setup import build_mcp_stack
17
+ from md_generator.media.youtube.api.settings import YouTubeApiSettings, cors_list
18
+ from md_generator.media.youtube.metadata import extract_video_id
19
+ from md_generator.media.youtube.service import YouTubeError, YouTubeToMarkdownService
20
+
21
+
22
+ class YouTubeConvertRequest(BaseModel):
23
+ url: str = Field(..., min_length=1, description="YouTube watch, youtu.be, shorts, or embed URL")
24
+ title: str | None = None
25
+ transcript_languages: list[str] | None = None
26
+ enable_audio_fallback: bool = True
27
+ whisper_model: str = "base"
28
+ language: str | None = Field(default=None, description="Whisper language if audio fallback runs")
29
+
30
+
31
+ def _validate_url(url: str) -> str:
32
+ u = url.strip()
33
+ if extract_video_id(u) is None:
34
+ raise HTTPException(status_code=400, detail="Invalid or unsupported YouTube URL")
35
+ return u
36
+
37
+
38
+ def create_app() -> FastAPI:
39
+ mcp, mcp_http = build_mcp_stack(mount_under_fastapi=True)
40
+
41
+ @asynccontextmanager
42
+ async def lifespan(application: FastAPI):
43
+ settings = YouTubeApiSettings()
44
+ base = Path(settings.temp_dir) if settings.temp_dir else None
45
+ store = MediaJobStore(base, settings.job_ttl_seconds)
46
+ store.start_sweeper()
47
+ application.state.settings = settings
48
+ application.state.job_store = store
49
+ async with mcp.session_manager.run():
50
+ yield
51
+
52
+ app = FastAPI(title="youtube-to-md", lifespan=lifespan)
53
+ app.mount("/mcp", mcp_http)
54
+
55
+ _bootstrap = YouTubeApiSettings()
56
+ app.add_middleware(
57
+ CORSMiddleware,
58
+ allow_origins=cors_list(_bootstrap),
59
+ allow_credentials="*" not in cors_list(_bootstrap),
60
+ allow_methods=["*"],
61
+ allow_headers=["*"],
62
+ )
63
+
64
+ @app.post("/convert/sync")
65
+ async def convert_sync(body: YouTubeConvertRequest) -> Response:
66
+ url = _validate_url(body.url)
67
+ with tempfile.TemporaryDirectory(prefix="yt-sync-") as td_name:
68
+ out = Path(td_name) / "transcript.md"
69
+ svc = YouTubeToMarkdownService(whisper_model=body.whisper_model, whisper_language=body.language)
70
+ try:
71
+ svc.write_markdown(
72
+ url,
73
+ out,
74
+ title=body.title,
75
+ transcript_languages=body.transcript_languages,
76
+ enable_audio_fallback=body.enable_audio_fallback,
77
+ )
78
+ except YouTubeError as e:
79
+ raise HTTPException(status_code=422, detail=str(e)) from e
80
+ md = out.read_text(encoding="utf-8")
81
+ return Response(
82
+ content=md.encode("utf-8"),
83
+ media_type="text/markdown; charset=utf-8",
84
+ headers={"Content-Disposition": 'attachment; filename="transcript.md"'},
85
+ )
86
+
87
+ @app.post("/convert/jobs")
88
+ async def convert_jobs(request: Request, body: YouTubeConvertRequest) -> dict:
89
+ url = _validate_url(body.url)
90
+ store: MediaJobStore = request.app.state.job_store
91
+ job = store.create_job()
92
+ out = job.workspace / "transcript.md"
93
+
94
+ def work() -> None:
95
+ svc = YouTubeToMarkdownService(whisper_model=body.whisper_model, whisper_language=body.language)
96
+ svc.write_markdown(
97
+ url,
98
+ out,
99
+ title=body.title,
100
+ transcript_languages=body.transcript_languages,
101
+ enable_audio_fallback=body.enable_audio_fallback,
102
+ )
103
+ job.result_path = out
104
+
105
+ store.run_async(job, work)
106
+ return {"job_id": job.job_id, "status": job.status}
107
+
108
+ @app.get("/convert/jobs/{job_id}")
109
+ async def job_status(request: Request, job_id: str) -> dict:
110
+ store: MediaJobStore = request.app.state.job_store
111
+ job = store.get(job_id)
112
+ if not job:
113
+ raise HTTPException(404, detail="Unknown job_id")
114
+ return {
115
+ "status": job.status,
116
+ "error": job.error,
117
+ "created_at": job.created_at,
118
+ }
119
+
120
+ @app.get("/convert/jobs/{job_id}/download", response_class=FileResponse)
121
+ async def job_download(request: Request, job_id: str) -> FileResponse:
122
+ store: MediaJobStore = request.app.state.job_store
123
+ job = store.get(job_id)
124
+ if not job:
125
+ raise HTTPException(404, detail="Unknown job_id")
126
+ if job.status != "done" or not job.result_path or not job.result_path.is_file():
127
+ raise HTTPException(400, detail="Job is not ready for download")
128
+ path = job.result_path
129
+ task = BackgroundTask(store.remove_after_download, job)
130
+ return FileResponse(
131
+ path,
132
+ media_type="text/markdown; charset=utf-8",
133
+ filename="transcript.md",
134
+ background=task,
135
+ )
136
+
137
+ return app
138
+
139
+
140
+ app = create_app()
@@ -0,0 +1,31 @@
1
+ """Standalone MCP server: stdio, SSE, or streamable HTTP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(description="youtube-to-md MCP server")
11
+ parser.add_argument(
12
+ "--transport",
13
+ choices=("stdio", "sse", "streamable-http"),
14
+ default="stdio",
15
+ )
16
+ args = parser.parse_args()
17
+
18
+ from md_generator.media.youtube.api.mcp_setup import build_mcp_stack
19
+
20
+ mcp, _ = build_mcp_stack(mount_under_fastapi=False)
21
+
22
+ if args.transport == "stdio":
23
+ asyncio.run(mcp.run_stdio_async())
24
+ elif args.transport == "sse":
25
+ asyncio.run(mcp.run_sse_async())
26
+ else:
27
+ asyncio.run(mcp.run_streamable_http_async())
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()