CoolFace
Apppublic

visualizingjp/pdf-ocr

sourceHugging Facegpl-3.0updated 4y agoView on Hugging Face
3likes
pdf2text.py404 linesDownload Raw Back to root
1# -*- coding: utf-8 -*-2"""3 4easyocr.py - A wrapper for easyocr to convert pdf to images to text5"""6 7import logging8from pathlib import Path9 10logging.basicConfig(11    level=logging.INFO,12    format="%(asctime)s %(levelname)s %(message)s",13    datefmt="%m/%d/%Y %I:%M:%S",14)15 16 17import os18import pprint as pp19import re20import shutil21import time22from datetime import date, datetime23from os.path import basename, dirname, join24from pathlib import Path25 26from cleantext import clean27from doctr.io import DocumentFile28from doctr.models import ocr_predictor29from libretranslatepy import LibreTranslateAPI30from natsort import natsorted31from spellchecker import SpellChecker32from tqdm.auto import tqdm33 34 35def simple_rename(filepath, target_ext=".txt"):36    _fp = Path(filepath)37    basename = _fp.stem38    return f"OCR_{basename}_{target_ext}"39 40 41def rm_local_text_files(name_contains="RESULT_"):42    """43    rm_local_text_files - remove local text files44 45    Args:46        name_contains (str, optional): [description]. Defaults to "OCR_".47    """48    files = [49        f50        for f in Path.cwd().iterdir()51        if f.is_file() and f.suffix == ".txt" and name_contains in f.name52    ]53    logging.info(f"removing {len(files)} text files")54    for f in files:55        os.remove(f)56    logging.info("done")57 58 59def corr(60    s: str,61    add_space_when_numerics=False,62    exceptions=["e.g.", "i.e.", "etc.", "cf.", "vs.", "p."],63) -> str:64    """corrects spacing in a string65 66    Args:67        s (str): the string to correct68        add_space_when_numerics (bool, optional): [add a space when a period is between two numbers, example 5.73]. Defaults to False.69        exceptions (list, optional): [do not change these substrings]. Defaults to ['e.g.', 'i.e.', 'etc.', 'cf.', 'vs.', 'p.'].70 71    Returns:72        str: the corrected string73    """74    if add_space_when_numerics:75        s = re.sub(r"(\d)\.(\d)", r"\1. \2", s)76 77    s = re.sub(r"\s+", " ", s)78    s = re.sub(r'\s([?.!"](?:\s|$))', r"\1", s)79 80    # fix space before apostrophe81    s = re.sub(r"\s\'", r"'", s)82    # fix space after apostrophe83    s = re.sub(r"'\s", r"'", s)84    # fix space before comma85    s = re.sub(r"\s,", r",", s)86 87    for e in exceptions:88        expected_sub = re.sub(r"\s", "", e)89        s = s.replace(expected_sub, e)90 91    return s92 93 94def fix_punct_spaces(string):95    """96    fix_punct_spaces - replace spaces around punctuation with punctuation. For example, "hello , there" -> "hello, there"97 98    Parameters99    ----------100    string : str, required, input string to be corrected101 102    Returns103    -------104    str, corrected string105    """106 107    fix_spaces = re.compile(r"\s*([?!.,]+(?:\s+[?!.,]+)*)\s*")108    string = fix_spaces.sub(lambda x: "{} ".format(x.group(1).replace(" ", "")), string)109    string = string.replace(" ' ", "'")110    string = string.replace(' " ', '"')111    return string.strip()112 113 114def clean_OCR(ugly_text: str):115    """116    clean_OCR - clean the OCR text files.117 118    Parameters119    ----------120    ugly_text : str, required, input string to be cleaned121 122    Returns123    -------124    str, cleaned string125    """126    # Remove all the newlines.127    cleaned_text = ugly_text.replace("\n", " ")128    # Remove all the tabs.129    cleaned_text = cleaned_text.replace("\t", " ")130    # Remove all the double spaces.131    cleaned_text = cleaned_text.replace("  ", " ")132    # Remove all the spaces at the beginning of the text.133    cleaned_text = cleaned_text.lstrip()134    # remove all instances of "- " and " - "135    cleaned_text = cleaned_text.replace("- ", "")136    cleaned_text = cleaned_text.replace(" -", "")137    return fix_punct_spaces(cleaned_text)138 139 140def move2completed(from_dir, filename, new_folder="completed", verbose=False):141 142    # this is the better version143    old_filepath = join(from_dir, filename)144 145    new_filedirectory = join(from_dir, new_folder)146 147    if not os.path.isdir(new_filedirectory):148        os.mkdir(new_filedirectory)149        if verbose:150            print("created new directory for files at: \n", new_filedirectory)151    new_filepath = join(new_filedirectory, filename)152 153    try:154        shutil.move(old_filepath, new_filepath)155        logging.info("successfully moved the file {} to */completed.".format(filename))156    except:157        logging.info(158            "ERROR! unable to move file to \n{}. Please investigate".format(159                new_filepath160            )161        )162 163 164"""## pdf2text functions165 166"""167 168 169custom_replace_list = {170    "t0": "to",171    "'$": "'s",172    ",,": ", ",173    "_ ": " ",174    " '": "'",175}176 177replace_corr_exceptions = {178    "i. e.": "i.e.",179    "e. g.": "e.g.",180    "e. g": "e.g.",181    " ,": ",",182}183 184 185spell = SpellChecker()186 187 188def check_word_spelling(word: str) -> bool:189    """190    check_word_spelling - check the spelling of a word191 192    Args:193        word (str): word to check194 195    Returns:196        bool: True if word is spelled correctly, False if not197    """198 199    misspelled = spell.unknown([word])200 201    return len(misspelled) == 0202 203 204def eval_and_replace(text: str, match_token: str = "- ") -> str:205    """206    eval_and_replace  - conditionally replace all instances of a substring in a string based on whether the eliminated substring results in a valid word207 208    Args:209        text (str): text to evaluate210        match_token (str, optional): token to replace. Defaults to "- ".211 212    Returns:213        str:  text with replaced tokens214    """215 216    if match_token not in text:217        return text218    else:219        while True:220            full_before_text = text.split(match_token, maxsplit=1)[0]221            before_text = [222                char for char in full_before_text.split()[-1] if char.isalpha()223            ]224            before_text = "".join(before_text)225            full_after_text = text.split(match_token, maxsplit=1)[-1]226            after_text = [char for char in full_after_text.split()[0] if char.isalpha()]227            after_text = "".join(after_text)228            full_text = before_text + after_text229            if check_word_spelling(full_text):230                text = full_before_text + full_after_text231            else:232                text = full_before_text + " " + full_after_text233            if match_token not in text:234                break235        return text236 237 238def cleantxt_ocr(ugly_text, lower=False, lang: str = "en") -> str:239    """240    cleantxt_ocr - clean text from OCR241 242    Args:243        ugly_text (str): text to clean244        lower (bool, optional): _description_. Defaults to False.245        lang (str, optional): _description_. Defaults to "en".246 247    Returns:248        str: cleaned text249    """250    # a wrapper for clean text with options different than default251 252    # https://pypi.org/project/clean-text/253    cleaned_text = clean(254        ugly_text,255        fix_unicode=True,  # fix various unicode errors256        to_ascii=True,  # transliterate to closest ASCII representation257        lower=lower,  # lowercase text258        no_line_breaks=True,  # fully strip line breaks as opposed to only normalizing them259        no_urls=True,  # replace all URLs with a special token260        no_emails=True,  # replace all email addresses with a special token261        no_phone_numbers=False,  # replace all phone numbers with a special token262        no_numbers=False,  # replace all numbers with a special token263        no_digits=False,  # replace all digits with a special token264        no_currency_symbols=False,  # replace all currency symbols with a special token265        no_punct=False,  # remove punctuations266        replace_with_punct="",  # instead of removing punctuations you may replace them267        replace_with_url="<URL>",268        replace_with_email="<EMAIL>",269        replace_with_phone_number="<PHONE>",270        replace_with_number="<NUM>",271        replace_with_digit="0",272        replace_with_currency_symbol="<CUR>",273        lang=lang,  # set to 'de' for German special handling274    )275 276    return cleaned_text277 278 279def format_ocr_out(OCR_data):280 281    if isinstance(OCR_data, list):282        text = " ".join(OCR_data)283    else:284        text = str(OCR_data)285    _clean = cleantxt_ocr(text)286    return corr(_clean)287 288 289def postprocess(text: str) -> str:290    """to be used after recombining the lines"""291 292    proc = corr(cleantxt_ocr(text))293 294    for k, v in custom_replace_list.items():295        proc = proc.replace(str(k), str(v))296 297    proc = corr(proc)298 299    for k, v in replace_corr_exceptions.items():300        proc = proc.replace(str(k), str(v))301 302    return eval_and_replace(proc)303 304 305def result2text(result, as_text=False) -> str or list:306    """Convert OCR result to text"""307 308    full_doc = []309    for i, page in enumerate(result.pages, start=1):310        text = ""311        for block in page.blocks:312            text += "\n\t"313            for line in block.lines:314                for word in line.words:315                    # print(dir(word))316                    text += word.value + " "317        full_doc.append(text)318 319    return "\n".join(full_doc) if as_text else full_doc320 321 322def convert_PDF_to_Text(323    PDF_file,324    ocr_model=None,325    max_pages: int = 20,326):327 328    st = time.perf_counter()329    PDF_file = Path(PDF_file)330    ocr_model = ocr_predictor(pretrained=True) if ocr_model is None else ocr_model331    logging.info(f"starting OCR on {PDF_file.name}")332    doc = DocumentFile.from_pdf(PDF_file)333    truncated = False334    if len(doc) > max_pages:335        logging.warning(336            f"PDF has {len(doc)} pages, which is more than {max_pages}.. truncating"337        )338        doc = doc[:max_pages]339        truncated = True340 341    # Analyze342    logging.info(f"running OCR on {len(doc)} pages")343    result = ocr_model(doc)344    raw_text = result2text(result)345    proc_text = [format_ocr_out(r) for r in raw_text]346    fin_text = [postprocess(t) for t in proc_text]347 348    ocr_results = "\n\n".join(fin_text)349 350    fn_rt = time.perf_counter() - st351 352    logging.info("OCR complete")353 354    results_dict = {355        "num_pages": len(doc),356        "runtime": round(fn_rt, 2),357        "date": str(date.today()),358        "converted_text": ocr_results,359        "truncated": truncated,360        "length": len(ocr_results),361    }362 363    return results_dict364 365 366# @title translation functions367 368lt = LibreTranslateAPI("https://translate.astian.org/")369 370 371def translate_text(text, source_l, target_l="en"):372 373    return str(lt.translate(text, source_l, target_l))374 375 376def translate_doc(filepath, lang_start, lang_end="en", verbose=False):377    """translate a document from lang_start to lang_end378 379        {'code': 'en', 'name': 'English'},380    {'code': 'fr', 'name': 'French'},381    {'code': 'de', 'name': 'German'},382    {'code': 'it', 'name': 'Italian'},"""383 384    src_folder = dirname(filepath)385    src_folder = Path(src_folder)386    trgt_folder = src_folder / f"translated_{lang_end}"387    trgt_folder.mkdir(exist_ok=True)388    with open(filepath, "r", encoding="utf-8", errors="ignore") as f:389        foreign_t = f.readlines()390    in_name = basename(filepath)391    translated_doc = []392    for line in tqdm(393        foreign_t, total=len(foreign_t), desc="translating {}...".format(in_name[:10])394    ):395        translated_line = translate_text(line, lang_start, lang_end)396        translated_doc.append(translated_line)397    t_out_name = "[To {}]".format(lang_end) + simple_rename(in_name) + ".txt"398    out_path = join(trgt_folder, t_out_name)399    with open(out_path, "w", encoding="utf-8", errors="ignore") as f_o:400        f_o.writelines(translated_doc)401    if verbose:402        print("finished translating the document! - ", datetime.now())403    return out_path404