Source code for indic_normalizer.latex

"""Pure-Python, English-only LaTeX-to-speech engine.

Public API
----------
    latex_to_speech(expr, verbosity="natural") -> str
    convert_spans(text, verbosity="natural") -> str
    LATEX_SPAN_PATTERN  (compiled re.Pattern)
"""

from __future__ import annotations

import re

from .parser import parse
from .speak import Speaker
from . import chemistry

__all__ = [
    "latex_to_speech",
    "convert_spans",
    "LATEX_SPAN_PATTERN",
]

def _strip_fallback(expr):
    """Best-effort degraded reading when parsing fails."""
    from .speak import _speak_number_str

    # Drop backslash commands, keep their names dropped entirely.
    s = re.sub(r"\\[A-Za-z]+", " ", expr)
    s = re.sub(r"[{}$^_&\\]", " ", s)
    out = []
    for tok in re.findall(r"\d+\.\d+|\d+|[A-Za-z]+|[+\-=<>]", s):
        if tok[0].isdigit():
            out.append(_speak_number_str(tok))
        elif tok in ("+", "-", "=", "<", ">"):
            out.append(
                {"+": "plus", "-": "minus", "=": "equals",
                 "<": "less than", ">": "greater than"}[tok]
            )
        else:
            out.append(tok)
    return re.sub(r"\s+", " ", " ".join(out)).strip()


[docs] def latex_to_speech(expr, verbosity="natural"): """Convert raw LaTeX (without delimiters) to spoken English. Never raises: on failure, degrades to a stripped reading. """ if expr is None: return "" try: tree = parse(expr) out = Speaker(verbosity).speak(tree) out = _scrub_markup(out) if out: return out return _scrub_markup(_strip_fallback(expr)) except Exception: try: return _scrub_markup(_strip_fallback(expr)) except Exception: return ""
def _scrub_markup(out): """Spoken output must never leak LaTeX markup, whatever the parser did. Also collapses stacked grouping markers from adjacent constructs.""" out = re.sub(r"[\\{}$^_~&]", " ", out) out = re.sub(r"\s+", " ", out).strip() return re.sub(r"\b(the quantity)( the quantity)+\b", r"\1", out) # -------------------------------------------------------------------------- # Span detection in mixed text # -------------------------------------------------------------------------- # Group numbering (fixed across the whole alternation): # 1: env name 2: env body # 3: $$...$$ # 4: \[...\] # 5: \(...\) # 6: \ce{...} # 7: $...$ LATEX_SPAN_PATTERN = re.compile( r"\\begin\{([A-Za-z*]+)\}(.*?)\\end\{\1\}" # 1,2 r"|\$\$(.+?)\$\$" # 3 r"|\\\[(.+?)\\\]" # 4 r"|\\\((.+?)\\\)" # 5 r"|\\ce\{([^{}]*)\}" # 6 r"|\$(?![\d\s])(.+?)\$", # 7 re.DOTALL, ) # A bare $...$ span is math only if its content looks mathy; otherwise the $ # is almost certainly currency (or a shell variable) and the span must be left # for the later semiotic-class handlers. The span pattern above already # refuses to OPEN at "$<digit>"; this gates what it still captures. _MATHY = re.compile(r"[\\^_={}]") def _is_math_content(s): if _MATHY.search(s): return True # short spaceless expressions with a letter variable: $n$, $x+y$, $a/b$ return " " not in s and len(s) <= 20 and re.search(r"[A-Za-z]", s) is not None
[docs] def convert_spans(text, verbosity="natural"): """Replace every LaTeX span in ``text`` with its spoken reading. Each span becomes ``" <spoken> "`` (space-padded). Non-LaTeX text is untouched. Never raises. """ if text is None: return "" def _repl(m): try: if m.group(1) is not None: # environment spoken = latex_to_speech(m.group(0), verbosity) elif m.group(3) is not None: # $$...$$ spoken = latex_to_speech(m.group(3), verbosity) elif m.group(4) is not None: # \[...\] spoken = latex_to_speech(m.group(4), verbosity) elif m.group(5) is not None: # \(...\) spoken = latex_to_speech(m.group(5), verbosity) elif m.group(6) is not None: # \ce{...} spoken = chemistry.read_ce(m.group(6)) elif m.group(7) is not None: # $...$ if not _is_math_content(m.group(7)): return m.group(0) # currency / prose: leave untouched spoken = latex_to_speech(m.group(7), verbosity) else: spoken = latex_to_speech(m.group(0), verbosity) except Exception: spoken = _strip_fallback(m.group(0)) return " " + spoken + " " try: return LATEX_SPAN_PATTERN.sub(_repl, text) except Exception: return text