Files
wehub-resource-sync 461bf6fd40
CI / lint (3.11) (push) Has been cancelled
CI / lint (3.12) (push) Has been cancelled
CI / lint (3.13) (push) Has been cancelled
CI / shellcheck (push) Has been cancelled
CI / shfmt (push) Has been cancelled
CI / setup (3.11) (push) Has been cancelled
CI / setup (3.12) (push) Has been cancelled
CI / setup (3.13) (push) Has been cancelled
CI / check-licenses (3.12) (push) Has been cancelled
CI / test_unit (3.11) (push) Has been cancelled
CI / test_unit (3.12) (push) Has been cancelled
CI / test_unit (3.13) (push) Has been cancelled
CI / test_unit_no_extras (3.11) (push) Has been cancelled
CI / test_unit_no_extras (3.12) (push) Has been cancelled
CI / test_json_to_html (3.12) (push) Has been cancelled
CI / test_unit_no_extras (3.13) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
Build And Push Docker Image / set-short-sha (push) Has been cancelled
Partition Benchmark / setup (push) Has been cancelled
Partition Benchmark / Measure and compare partition() runtime (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Has been cancelled
CI / test_ingest_src (3.12) (push) Has been cancelled
CI / test_json_to_markdown (3.12) (push) Has been cancelled
CI / changelog (push) Has been cancelled
CI / test_dockerfile (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build And Push Docker Image / publish-images (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:56 +08:00

584 lines
19 KiB
Python

from __future__ import annotations
import re
from functools import lru_cache
from typing import Callable, Iterable, Iterator, Optional
import iso639 # pyright: ignore[reportMissingTypeStubs]
from langdetect import ( # pyright: ignore[reportMissingTypeStubs]
DetectorFactory,
detect_langs, # pyright: ignore[reportUnknownVariableType]
lang_detect_exception,
)
from unstructured.documents.elements import Element
from unstructured.logger import logger
from unstructured.partition.utils.constants import (
TESSERACT_LANGUAGES_AND_CODES,
TESSERACT_LANGUAGES_SPLITTER,
)
_ASCII_RE = re.compile(r"^[\x00-\x7F]+$")
# pytesseract.get_languages(config="") only shows user installed language packs,
# so manually include the list of all currently supported Tesseract languages
PYTESSERACT_LANG_CODES = [
"afr",
"amh",
"ara",
"asm",
"aze",
"aze_cyrl",
"bel",
"ben",
"bod",
"bos",
"bre",
"bul",
"cat",
"ceb",
"ces",
"chi_sim",
"chi_sim_vert",
"chi_tra",
"chi_tra_vert",
"chr",
"cos",
"cym",
"dan",
"deu",
"div",
"dzo",
"ell",
"eng",
"enm",
"epo",
"equ",
"est",
"eus",
"fao",
"fas",
"fil",
"fin",
"fra",
"frk",
"frm",
"fry",
"gla",
"gle",
"glg",
"grc",
"guj",
"hat",
"heb",
"hin",
"hrv",
"hun",
"hye",
"iku",
"ind",
"isl",
"ita",
"ita_old",
"jav",
"jpn",
"jpn_vert",
"kan",
"kat",
"kat_old",
"kaz",
"khm",
"kir",
"kmr",
"kor",
"kor_vert",
"lao",
"lat",
"lav",
"lit",
"ltz",
"mal",
"mar",
"mkd",
"mlt",
"mon",
"mri",
"msa",
"mya",
"nep",
"nld",
"nor",
"oci",
"ori",
"osd",
"pan",
"pol",
"por",
"pus",
"que",
"ron",
"rus",
"san",
"sin",
"slk",
"slv",
"snd",
"snum",
"spa",
"spa_old",
"sqi",
"srp",
"srp_latn",
"sun",
"swa",
"swe",
"syr",
"tam",
"tat",
"tel",
"tgk",
"tha",
"tir",
"ton",
"tur",
"uig",
"ukr",
"urd",
"uzb",
"uzb_cyrl",
"vie",
"yid",
"yor",
]
PYTESSERACT_TO_PADDLE_LANG_CODE_MAP = {
"afr": "af", # Afrikaans
"ara": "ar", # Arabic
"aze": "az", # Azerbaijani
"bel": "be", # Belarusian
"bos": "bs", # Bosnian
"bul": "bg", # Bulgarian
"ces": "cs", # Czech
"chi_sim": "ch", # Simplified Chinese
"chi_tra": "chinese_cht", # Traditional Chinese
"cym": "cy", # Welsh
"dan": "da", # Danish
"deu": "german", # German
"eng": "en", # English
"est": "et", # Estonian
"fas": "fa", # Persian
"fra": "fr", # French
"gle": "ga", # Irish
"hin": "hi", # Hindi
"hrv": "hr", # Croatian
"hun": "hu", # Hungarian
"ind": "id", # Indonesian
"isl": "is", # Icelandic
"ita": "it", # Italian
"jpn": "japan", # Japanese
"kor": "korean", # Korean
"kmr": "ku", # Kurdish
"lat": "rs_latin", # Latin
"lav": "lv", # Latvian
"lit": "lt", # Lithuanian
"mar": "mr", # Marathi
"mlt": "mt", # Maltese
"msa": "ms", # Malay
"nep": "ne", # Nepali
"nld": "nl", # Dutch
"nor": "no", # Norwegian
"pol": "pl", # Polish
"por": "pt", # Portuguese
"ron": "ro", # Romanian
"rus": "ru", # Russian
"slk": "sk", # Slovak
"slv": "sl", # Slovenian
"spa": "es", # Spanish
"sqi": "sq", # Albanian
"srp": "rs_cyrillic", # Serbian
"swa": "sw", # Swahili
"swe": "sv", # Swedish
"tam": "ta", # Tamil
"tel": "te", # Telugu
"tur": "tr", # Turkish
"uig": "ug", # Uyghur
"ukr": "uk", # Ukrainian
"urd": "ur", # Urdu
"uzb": "uz", # Uzbek
"vie": "vi", # Vietnamese
}
def prepare_languages_for_tesseract(languages: Optional[list[str]] = ["eng"]) -> str:
"""
Entry point: convert languages (list of strings) into tesseract ocr langcode format (uses +)
"""
if languages is None:
raise ValueError("`languages` can not be `None`")
converted_languages = [
lang_code
for lang_code in (
_convert_language_code_to_pytesseract_lang_code(lang) for lang in languages
)
if lang_code
]
# Remove duplicates from the list but keep the original order
converted_languages = list(dict.fromkeys(converted_languages))
if len(converted_languages) == 0:
logger.warning(
"Failed to find any valid standard language code from "
f"languages: {languages}, proceed with `eng` instead.",
)
return "eng"
return TESSERACT_LANGUAGES_SPLITTER.join(converted_languages)
def tesseract_to_paddle_language(tesseract_language: str) -> str:
"""
Convert TesseractOCR language code to PaddleOCR language code.
:param tesseract_language: str, language code used in TesseractOCR
:return: str, corresponding language code for PaddleOCR or None if not found
"""
lang = PYTESSERACT_TO_PADDLE_LANG_CODE_MAP.get(tesseract_language.lower())
if not lang:
logger.warning(
f"{tesseract_language} is not a language code supported by PaddleOCR, "
f"proceeding with `en` instead."
)
return "en"
return lang
def check_language_args(
languages: list[str], ocr_languages: str | list[str] | None
) -> list[str] | None:
"""Handle users defining both `ocr_languages` and `languages`.
Give preference to `languages` and convert `ocr_languages` if needed, but default to `None`.
`ocr_languages` is only a parameter for `auto.partition`, `partition_image`, & `partition_pdf`.
`ocr_languages` should not be defined as 'auto' since 'auto' is intended for language detection
which is not supported by `partition_image` or `partition_pdf`.
"""
# --- Clean and update defaults
if ocr_languages:
ocr_languages = _clean_ocr_languages_arg(ocr_languages)
logger.warning(
"The ocr_languages kwarg will be deprecated in a future version of unstructured. "
"Please use languages instead.",
)
assert ocr_languages is None or isinstance(ocr_languages, str)
if ocr_languages and "auto" in ocr_languages:
raise ValueError(
"`ocr_languages` is deprecated but was used to extract text from pdfs and images."
" The 'auto' argument is only for language *detection* when it is assigned"
" to `languages` and partitioning documents other than pdfs or images."
" Language detection is not currently supported in pdfs or images."
)
if not isinstance(languages, list): # pyright: ignore[reportUnnecessaryIsInstance]
raise TypeError(
"The language parameter must be a list of language codes as strings, ex. ['eng']",
)
# --- If `languages` is a null/default value and `ocr_languages` is defined, use `ocr_languages`
if ocr_languages and (languages == ["auto"] or languages == [""] or not languages):
languages = ocr_languages.split(TESSERACT_LANGUAGES_SPLITTER)
logger.warning(
"Only one of languages and ocr_languages should be specified. "
"languages is preferred. ocr_languages is marked for deprecation.",
)
# --- Clean `languages`
# If "auto" is included in the list of inputs, language detection will be triggered downstream.
# The rest of the inputted languages are ignored.
if languages:
if "auto" not in languages:
for i, lang in enumerate(languages):
languages[i] = TESSERACT_LANGUAGES_AND_CODES.get(lang.lower(), lang)
str_languages = _clean_ocr_languages_arg(languages)
if not str_languages:
return None
languages = str_languages.split(TESSERACT_LANGUAGES_SPLITTER)
# else, remove the extraneous languages.
# NOTE (jennings): "auto" should only be used for partitioners OTHER THAN `_pdf` or `_image`
else:
# define as 'auto' for language detection when partitioning non-pdfs or -images
languages = ["auto"]
return languages
return None
def convert_old_ocr_languages_to_languages(ocr_languages: str) -> list[str]:
"""
Convert ocr_languages parameter to list of langcode strings.
Assumption: ocr_languages is in tesseract plus sign format
"""
return ocr_languages.split(TESSERACT_LANGUAGES_SPLITTER)
def _convert_language_code_to_pytesseract_lang_code(lang: str) -> str:
"""
Convert a single language code to its tesseract formatted and recognized
langcode(s), if supported.
"""
# if language is already tesseract langcode, return it immediately
# this will catch the tesseract special cases equ and osd
# NOTE(shreya): this may catch some cases of choosing between tesseract code variants for a lang
if lang in PYTESSERACT_LANG_CODES:
return lang
lang_iso639 = _get_iso639_language_object(lang)
# tesseract uses 3 digit codes (639-3, 639-2b, etc) as prefixes, with suffixes for orthography
# use first 3 letters of tesseract codes for matching to standard codes
pytesseract_langs_3 = {lang[:3] for lang in PYTESSERACT_LANG_CODES}
if lang_iso639:
# try to match ISO 639-3 code
if lang_iso639.part3 in pytesseract_langs_3:
matched_langcodes = _get_all_tesseract_langcodes_with_prefix(lang_iso639.part3)
return TESSERACT_LANGUAGES_SPLITTER.join(matched_langcodes)
# try to match ISO 639-2b
elif lang_iso639.part2b in pytesseract_langs_3:
matched_langcodes = _get_all_tesseract_langcodes_with_prefix(lang_iso639.part2b)
return TESSERACT_LANGUAGES_SPLITTER.join(matched_langcodes)
# try to match ISO 639-2t
elif lang_iso639.part2t in pytesseract_langs_3:
matched_langcodes = _get_all_tesseract_langcodes_with_prefix(lang_iso639.part2t)
return TESSERACT_LANGUAGES_SPLITTER.join(matched_langcodes)
else:
logger.warning(f"{lang} is not a language supported by Tesseract.")
return ""
logger.warning(f"{lang} is not a language supported by Tesseract.")
return ""
def _get_iso639_language_object(lang: str) -> Optional[iso639.Language]:
language = _cached_iso639_language_match(lang)
if language is not None:
return language
logger.warning(f"{lang} is not a valid standard language code.")
return None
def _get_all_tesseract_langcodes_with_prefix(prefix: str) -> list[str]:
"""
Get all matching tesseract langcodes with this prefix (may be one or multiple variants).
"""
return [langcode for langcode in PYTESSERACT_LANG_CODES if langcode.startswith(prefix)]
def _validate_fallback_languages(
value: Optional[list[str]],
) -> Optional[list[str]]:
"""Validate and normalize language_fallback return value to ISO 639-3 codes.
Returns None for None, non-list, or when no valid codes remain (invalid entries
are logged and skipped).
"""
if value is None:
return None
if not isinstance(value, list):
logger.warning(
f"language_fallback must return None or a list of strings, got {type(value).__name__}."
)
return None
validated: list[str] = []
for item in value:
if not isinstance(item, str) or not item.strip():
continue
lang = item.strip()
if lang == "zho":
validated.append("zho")
else:
language = _get_iso639_language_object(lang[:3])
if language is not None:
validated.append(language.part3)
return validated if validated else None
def detect_languages(
text: str,
languages: Optional[list[str]] = None,
language_fallback: Optional[Callable[[str], Optional[list[str]]]] = None,
) -> Optional[list[str]]:
"""
Detects the list of languages present in the text (in the default "auto" mode),
or formats and passes through the user inputted document languages if provided.
For short ASCII text (fewer than 5 words), language detection is unreliable. By
default such text is assigned English (["eng"]). Use ``language_fallback`` to
override: pass a callable that takes the text and returns a list of ISO 639-3
codes or None. Return None to leave language unspecified. The caller is
responsible for returning valid ISO 639-3 codes (e.g. "eng", "fra"); invalid
entries are filtered out and a warning is logged; if none remain, this function
returns None.
"""
if languages is None:
languages = ["auto"]
if not isinstance(languages, list):
raise TypeError(
'The language parameter must be a list of language codes as strings, ex. ["eng"]',
)
# Skip language detection for partitioners that use other partitioners.
# For example, partition_msg relies on partition_html and partition_text, but the metadata
# gets overwritten after elements have been returned by _html and _text,
# so `languages` would be detected twice.
# Also return None if there is no text.
if languages[0] == "" or text.strip() == "":
return None
# If text contains special characters (like ñ, å, or Korean/Mandarin/etc.) it will NOT default
# to English. It will default to English if text is only ascii characters and is short.
if _ASCII_RE.match(text) and len(text.split()) < 5:
if language_fallback is not None:
return _validate_fallback_languages(language_fallback(text))
logger.debug(f'short text: "{text}". Defaulting to English.')
return ["eng"]
# set seed for deterministic langdetect outputs
DetectorFactory.seed = 0
doc_languages: list[str] = []
# user inputted languages:
# if "auto" is included in the list of inputs, language detection will be triggered
# and the rest of the inputted languages will be ignored
if languages and "auto" not in languages:
for lang in languages:
str_lang = TESSERACT_LANGUAGES_AND_CODES.get(lang.lower(), lang)
language = _get_iso639_language_object(str_lang[:3])
if language:
doc_languages.append(language.part3)
# language detection:
else:
# warn if any values other than "auto" were provided
if len(languages) > 1:
logger.warning(
f'Since "auto" is present in the input languages provided ({languages}), '
"the language will be auto detected and the rest of the inputted "
"languages will be ignored.",
)
try:
langdetect_result = detect_langs(text)
except lang_detect_exception.LangDetectException as e:
logger.warning(e)
return None # None as default
langdetect_langs: list[str] = []
# NOTE(robinson) - Chinese gets detected with codes zh-cn, zh-tw, zh-hk for various
# Chinese variants. We normalizes these because there is a single model for Chinese
# machine translation
# TODO(shreya): decide how to maintain nonstandard chinese script information
for langobj in langdetect_result:
lang_val = str(langobj.lang)
if lang_val.startswith("zh"): # pyright: ignore
langdetect_langs.append("zho")
else:
language = _get_iso639_language_object(lang_val[:3]) # pyright: ignore
if language:
langdetect_langs.append(language.part3)
# remove duplicate chinese (if exists) without modifying order
seen = set(doc_languages)
for lang in langdetect_langs:
if lang not in seen:
doc_languages.append(lang)
seen.add(lang)
return doc_languages
def apply_lang_metadata(
elements: Iterable[Element],
languages: Optional[list[str]],
detect_language_per_element: bool = False,
language_fallback: Optional[Callable[[str], Optional[list[str]]]] = None,
) -> Iterator[Element]:
"""Detect language and apply it to metadata.languages for each element in `elements`.
If languages is None, default to auto detection.
If languages is an empty string, skip.
language_fallback is used for short text when detection is unreliable; see detect_languages."""
# -- Note this function has a stream interface, but reads the full `elements` stream into memory
# -- before emitting the first updated element as output.
# The auto `partition` function uses `None` as a default because the default for
# `partition_pdf` and `partition_img` conflict with the other partitioners that use ["auto"]
if languages is None:
languages = ["auto"]
# Skip language detection for partitioners that use other partitioners.
# For example, partition_msg relies on partition_html and partition_text, but the metadata
# gets overwritten after elements have been returned by _html and _text,
# so `languages` would be detected twice.
if languages == [""]:
yield from elements
return
# Convert elements to a list to get the text, detect the language, and add it to the elements
if not isinstance(elements, list):
elements = list(elements)
def detect(text: str) -> Optional[list[str]]:
return detect_languages(text=text, languages=languages, language_fallback=language_fallback)
full_text = " ".join(str(e.text) for e in elements if hasattr(e, "text") and e.text)
detected_languages = detect(full_text)
if (
detected_languages is not None
and len(detected_languages) == 1
and detect_language_per_element is False
):
# -- apply detected language to each element's metadata --
for e in elements:
e.metadata.languages = detected_languages
yield e
else:
for e in elements:
if hasattr(e, "text"):
text_value = str(e.text) if e.text is not None else ""
e.metadata.languages = detect(text_value)
yield e
else:
yield e
def _clean_ocr_languages_arg(ocr_languages: list[str] | str) -> str:
"""Fix common incorrect definitions for ocr_languages:
defining it as a list, adding extra quotation marks, adding brackets.
Returns a single string of ocr_languages"""
# extract from list
if isinstance(ocr_languages, list):
ocr_languages = "+".join(ocr_languages)
# remove extra quotations
ocr_languages = re.sub(r"[\"']", "", ocr_languages)
# remove brackets
ocr_languages = re.sub(r"[\[\]]", "", ocr_languages)
return ocr_languages
@lru_cache(maxsize=256)
def _cached_iso639_language_match(lang: str) -> Optional[iso639.Language]:
try:
return iso639.Language.match(lang.lower()) # pyright: ignore[reportUnknownMemberType]
except iso639.LanguageNotFoundError:
return None