Source code for md2star.twin_cli

"""``md2star twin``: read any document back into an editable Markdown twin.

The forward path turns Markdown into a polished DOCX/PPTX/PDF. This is the
inverse *as a document*, not just as text: given a PDF (or anything LibreOffice
can convert to one), it recovers prose + GFM tables, scrapes every embedded
raster back out as PNGs, and (with ``--diagrams``) re-authors node-and-edge
figures as Mermaid via the target-matching eyeball loop. The result is a
``<stem>.md`` plus an ``assets/`` folder you can edit and re-render.

Thin adapter over :func:`md2star.reverse.to_markdown_twin` and
:func:`md2star.reverse_diagrams.make_diagram_handler`; kept out of
:mod:`md2star.cli` so the reverse feature imports lazily and the ``md2star
twin`` command stays a one-liner. The flags are parsed by click, there, once —
this module receives them typed.


Author
------
[Warith HARCHAOUI](https://linkedin.com/in/warith-harchaoui/)
"""

from __future__ import annotations

from pathlib import Path

from .errors import Md2starError
from .logging import configure as configure_logging
from .logging import get_logger

# Module logger — child of the root "md2star" logger (configured by the CLI).
logger = get_logger(__name__)


[docs] def run( input_path: str, *, out_dir: str | None = None, no_images: bool = False, diagrams: bool = False, model: str | None = None, max_iterations: int = 3, verbose: bool = False, quiet: bool = False, ) -> int: """Recover *input_path* as an editable Markdown twin. Returns an exit code. Parameters ---------- input_path : str Document to recover: a PDF, or anything LibreOffice can print to one. out_dir : str or None, optional Where ``<stem>.md`` + ``assets/`` are written. An output *folder* rather than a file, because the twin is two things. ``None`` puts them next to the input. no_images : bool, optional Recover prose + tables only, dropping the scraped rasters. diagrams : bool, optional Re-author figures through the local VLM eyeball loop — node-and-edge diagrams as Mermaid, other vector figures as SVG. The one option that pulls in the AI stack, hence off by default. model : str or None, optional Vision model tag for *diagrams*. ``None`` lets best-engine-ai-helper pick per host. max_iterations : int, optional Cap on render→compare→revise cycles, so a stubborn diagram cannot spin forever. verbose, quiet : bool, optional Logging threshold, wired through the shared configuration. Returns ------- int ``0`` and the twin's path on stdout; ``2`` for a missing input, ``127`` for a missing system tool, ``1`` for any other recovery failure. """ # First statement, before any step can emit a diagnostic, so -v/-q apply # to the whole recovery rather than to whatever happens after this line. configure_logging(verbose=verbose, quiet=quiet) # Not resolved: a relative input keeps the name the user typed in the # "report.md written next to report.pdf" message below. src = Path(input_path) # Default the output next to the input so a bare ``md2star twin report.pdf`` # drops ``report.md`` + ``assets/`` right where the source lives. out = Path(out_dir) if out_dir else (src.parent if src.parent != Path("") else Path(".")) # Only build the AI handler when --diagrams is requested; without it the twin # is fully deterministic and needs neither Ollama nor best-engine. image_handler = None if diagrams: from .reverse_diagrams import diagrams_available, make_diagram_handler if not diagrams_available(model): # Don't fail the whole run — warn and continue with plain PNGs, since # the deterministic twin is still valuable without reconstruction. logger.warning( "md2star twin: --diagrams requested but the AI stack is unavailable " "(Ollama not installed/running, or the model isn't pulled); " "keeping scraped images as PNGs." ) else: image_handler = make_diagram_handler(model=model, max_iterations=max_iterations) # Imported here rather than at module scope: `reverse` pulls in Kreuzberg # (a Rust core plus OCR models), and `md2star twin --help` must not. try: from .reverse import to_markdown_twin md_path = to_markdown_twin( src, out, extract_images=not no_images, image_handler=image_handler, ) except FileNotFoundError as exc: logger.error("md2star twin: %s", exc) return 2 except Md2starError as exc: # Typed errors (e.g. LibreOffice missing for a non-native input) carry a # headline + hint; surface both without a traceback. logger.error("md2star twin: %s", exc) if getattr(exc, "hint", None): logger.error(" %s", exc.hint) return 127 except RuntimeError as exc: logger.error("md2star twin: %s", exc) return 1 else: # Program output → stdout so the path is pipeable; logs went to stderr. print(md_path) return 0