Files
2026-07-13 12:33:19 +08:00

161 lines
4.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
any_to_md.py — Convert any file to Markdown using Microsoft markitdown.
Supports: PDF, DOCX, PPTX, XLSX, XLS, HTML, CSV, JSON, XML, EPub, ZIP,
images (EXIF + optional LLM description), audio (with transcription),
YouTube URLs (with auto subtitles), Outlook .msg, and more.
Part of huashu-md-html skill — md is source, html is product.
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
HELP_INSTALL = """\
markitdown is not installed. Install it with:
pip install 'markitdown[all]'
Or, for a slimmer install (only the formats you need):
pip install 'markitdown[pdf,docx,pptx,xlsx]'
"""
def ensure_markitdown():
try:
from markitdown import MarkItDown # noqa: F401
except ImportError:
sys.stderr.write(HELP_INSTALL)
sys.exit(2)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Convert any file (or URL) to Markdown via Microsoft markitdown.",
)
p.add_argument(
"source",
help="File path or URL (http(s)://, file://, data:, YouTube URL).",
)
p.add_argument(
"-o", "--output",
help="Output .md path. If omitted, writes to <source-stem>.md in CWD; use '-' for stdout.",
)
p.add_argument(
"--llm-describe",
action="store_true",
help="Enable LLM-based image description. Requires OPENAI_API_KEY.",
)
p.add_argument(
"--llm-model",
default=os.environ.get("MARKITDOWN_LLM_MODEL", "gpt-4o"),
help="LLM model for image description (default: gpt-4o, or env MARKITDOWN_LLM_MODEL).",
)
p.add_argument(
"--azure-doc-intel",
default=os.environ.get("AZURE_DOC_INTEL_ENDPOINT"),
help="Azure Document Intelligence endpoint for high-fidelity PDF OCR.",
)
p.add_argument(
"--enable-plugins",
action="store_true",
help="Enable third-party markitdown plugins.",
)
p.add_argument(
"--quiet",
action="store_true",
help="Suppress non-error stderr output.",
)
return p.parse_args()
def build_converter(args: argparse.Namespace):
from markitdown import MarkItDown
kwargs = {"enable_plugins": args.enable_plugins}
if args.llm_describe:
try:
from openai import OpenAI
except ImportError:
sys.stderr.write(
"--llm-describe requires the 'openai' package: pip install openai\n",
)
sys.exit(2)
if not os.environ.get("OPENAI_API_KEY"):
sys.stderr.write(
"--llm-describe requires OPENAI_API_KEY environment variable.\n",
)
sys.exit(2)
kwargs["llm_client"] = OpenAI()
kwargs["llm_model"] = args.llm_model
if args.azure_doc_intel:
kwargs["docintel_endpoint"] = args.azure_doc_intel
return MarkItDown(**kwargs)
def resolve_output_path(source: str, output: str | None) -> Path | None:
if output == "-":
return None
if output:
return Path(output)
# Default: <source-stem>.md in CWD
if source.startswith(("http://", "https://", "data:", "file://")):
# URL → use a generic name
return Path("converted.md")
return Path(Path(source).stem + ".md")
def warn_known_pitfalls(source: str, content: str, quiet: bool) -> None:
if quiet:
return
suffix = Path(source).suffix.lower() if "://" not in source else ""
if suffix == ".pdf" and len(content.strip()) < 200:
sys.stderr.write(
"[hint] Output is very short — this PDF may be a scanned document.\n"
" Try --llm-describe or --azure-doc-intel for OCR.\n",
)
if suffix in {".pptx", ".ppt"}:
sys.stderr.write(
"[hint] PPTX text + speaker notes preserved; animations and layout are dropped.\n",
)
def main() -> int:
ensure_markitdown()
args = parse_args()
converter = build_converter(args)
try:
result = converter.convert(args.source)
except Exception as exc: # noqa: BLE001 — markitdown wraps various errors
sys.stderr.write(f"[error] markitdown failed: {exc}\n")
return 1
content = result.text_content or ""
out_path = resolve_output_path(args.source, args.output)
if out_path is None:
sys.stdout.write(content)
else:
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(content, encoding="utf-8")
if not args.quiet:
sys.stderr.write(f"[ok] {args.source}{out_path}\n")
warn_known_pitfalls(args.source, content, args.quiet)
return 0
if __name__ == "__main__":
raise SystemExit(main())