Team Ai
Apppublic

PitGlobal/PDF_Layout_Inspector

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py554 linesDownload Raw Back to root
1import flet as ft2import os3import sys4import glob5import json6import threading7import zipfile8 9sys.path.insert(0, os.path.join(os.path.dirname(__file__), "backend"))10 11 12from ocr_loader import run_ocr13from line_detector import run_line_detection14from text_line_checker import run_text_line_check15 16# ─────────────────────────────────────────────17# DIRECTORIES18# Uploaded PDFs land in UPLOAD_DIR (managed by Flet).19# Processed images are saved in OUTPUT_DIR, which lives inside20# ASSETS_DIR so Flet serves them as static files at /output/<name>.png21# ─────────────────────────────────────────────22UPLOAD_DIR = "uploads"23ASSETS_DIR = "assets"24OUTPUT_DIR = os.path.join(ASSETS_DIR, "output")25 26for _d in [UPLOAD_DIR, ASSETS_DIR, OUTPUT_DIR]:27    os.makedirs(_d, exist_ok=True)28 29 30# ─────────────────────────────────────────────31# PIPELINE  — processes one PDF through all 4 steps32# ─────────────────────────────────────────────33def process_pdf(pdf_path: str, pdf_name: str, log_fn, done_fn):34    """35    Runs the full inspection pipeline on a single PDF.36    log_fn(msg)              → called for each log line37    done_fn(name, paths, ok) → called when done (paths = list of output image paths)38    """39    try:40        # ── STEP 1: OCR ──────────────────────────────41        log_fn(f"[{pdf_name}]  Step 1 — AWS Textract OCR...")42        run_ocr(pdf_path)43 44        with open("output.json", encoding="utf-8") as f:45            aws_data = json.load(f)46 47        num_pages = len(aws_data)48        log_fn(f"[{pdf_name}]  {num_pages} page(s) found")49 50        output_paths = []51 52        for i in range(num_pages):53            page_num = i + 154            image_file = f"pagina_{page_num}.png"55 56            # ── STEP 2: Line detection ────────────────57            log_fn(f"[{pdf_name}]  Page {page_num} — Step 2: Line detection...")58            run_line_detection(image_file)59 60            # ── STEP 3: Text-line check ───────────────61            log_fn(f"[{pdf_name}]  Page {page_num} — Step 3: Text-line check...")62            error_image = run_text_line_check(image_file, page_index=i)63 64            out_name = f"{pdf_name}_{page_num}.png" if num_pages > 1 else f"{pdf_name}.png"65            out_path = os.path.join(OUTPUT_DIR, out_name)66            os.replace(error_image, out_path)   # oppure shutil.copyfile(error_image, out_path)67            output_paths.append(out_path)68 69 70        # ── CLEANUP intermediate files ────────────────71        for f in (72            glob.glob("pagina_*.png")73            + glob.glob("linee_*.json")74            +[75                "output.json",76                "debug_righe.png",77            ]78        ):79            if os.path.exists(f):80                os.remove(f)81 82        log_fn(f"[{pdf_name}]  Done — {len(output_paths)} result image(s) ready.")83        done_fn(pdf_name, output_paths, success=True)84 85    except Exception as ex:86        log_fn(f"[{pdf_name}]  ERROR: {ex}")87        done_fn(pdf_name, [], success=False)88 89def create_download_helper(out_name: str, img_url: str) -> str:90    """Crea un file HTML in assets che auto-triggera il download dell'immagine."""91    html = (92        f'<!DOCTYPE html><html><body>'93        f'<script>var a=document.createElement("a");a.href="{img_url}";'94        f'a.download="{out_name}";document.body.appendChild(a);a.click();</script>'95        f'<p>Downloading {out_name}... you can close this tab.</p>'96        f'</body></html>'97    )98    with open(os.path.join(ASSETS_DIR, f"dl_{out_name}.html"), "w") as f:99        f.write(html)100    return f"/dl_{out_name}.html"101 102# ─────────────────────────────────────────────103# FLET APP104# ─────────────────────────────────────────────105def main(page: ft.Page):106 107    page.title = "PDF Layout Inspector"108    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER109    page.bgcolor = "#F8FAFC"110    page.padding = ft.padding.symmetric(horizontal=40, vertical=40)111    page.scroll = ft.ScrollMode.AUTO112 113    # ── State ────────────────────────────────────────114    selected_files: list[str] = []   115    pending_uploads: set[str] = set()116 117    upload_map: dict[str, str] = {}118 119    # ─────────────────────────────────────────────────120    # UI COMPONENTS121    # ─────────────────────────────────────────────────122 123    # ── Log box ──────────────────────────────────────124    log_col = ft.Column(spacing=2, scroll=ft.ScrollMode.AUTO)125    log_box = ft.Container(126        content=log_col,127        bgcolor="#0F172A",128        border_radius=10,129        padding=ft.padding.all(14),130        height=210,131        width=700,132        visible=False,133    )134 135    def add_log(msg: str):136        log_col.controls.append(137            ft.Text(138                f"› {msg}",139                size=11,140                color="#86EFAC",141                selectable=True,142                font_family="Courier New",143            )144        )145        log_box.visible = True146        page.update()147 148    # ── File list ─────────────────────────────────────149    file_list_col = ft.Column(spacing=6, width=700)150    upload_counter = ft.Text("", size=13, color="#4F46E5", weight=ft.FontWeight.W_500)151 152    def refresh_files():153        file_list_col.controls.clear()154        for fp in selected_files:155            name = os.path.basename(fp)156            file_list_col.controls.append(157                ft.Container(158                    content=ft.Row(159                        [160                            ft.Icon(161                                ft.icons.PICTURE_AS_PDF_ROUNDED,162                                color="#EF4444",163                                size=20,164                            ),165                            ft.Text(166                                name,167                                size=13,168                                color="#1E293B",169                                expand=True,170                            ),171                            ft.Icon(172                                ft.icons.CHECK_CIRCLE_OUTLINE_ROUNDED,173                                color="#22C55E",174                                size=16,175                            ),176                        ],177                        spacing=10,178                    ),179                    bgcolor="#F1F5F9",180                    border_radius=8,181                    padding=ft.padding.symmetric(horizontal=14, vertical=10),182                )183            )184        start_btn.disabled = len(selected_files) == 0185        n = len(selected_files)186        upload_counter.value = f"{n} document{'s' if n != 1 else ''} uploaded" if n > 0 else ""187        page.update()188 189    # ── Results section ───────────────────────────────190    results_col = ft.Column(191        spacing=20,192        horizontal_alignment=ft.CrossAxisAlignment.CENTER,193    )194 195    def add_result_card(pdf_name: str, out_paths: list, success: bool):196        if not success or not out_paths:197            results_col.controls.append(198                ft.Container(199                    content=ft.Row(200                        [201                            ft.Icon(ft.icons.ERROR_OUTLINE_ROUNDED, color="#EF4444"),202                            ft.Text(203                                f"{pdf_name} — processing failed.",204                                color="#EF4444",205                                size=13,206                            ),207                        ],208                        spacing=8,209                    ),210                    width=700,211                    padding=ft.padding.symmetric(horizontal=10, vertical=8),212                    bgcolor="#FEF2F2",213                    border_radius=8,214                    border=ft.border.all(1, "#FECACA"),215                )216            )217            page.update()218            return219 220        for out_path in out_paths:221            out_name = os.path.basename(out_path)222            # Flet serves assets_dir files → /output/<name>.png223            img_url = f"/output/{out_name}"224 225            card = ft.Card(226                content=ft.Container(227                    content=ft.Column(228                        [229                            # Card header230                            ft.Row(231                                [232                                    ft.Icon(233                                        ft.icons.FIND_IN_PAGE_ROUNDED,234                                        color="#4F46E5",235                                        size=20,236                                    ),237                                    ft.Text(238                                        out_name,239                                        size=14,240                                        weight=ft.FontWeight.W_600,241                                        color="#1E293B",242                                        expand=True,243                                    ),244                                    ft.Container(245                                        content=ft.Text(246                                            "Inspected",247                                            size=11,248                                            color="#16A34A",249                                            weight=ft.FontWeight.W_500,250                                        ),251                                        bgcolor="#DCFCE7",252                                        border_radius=20,253                                        padding=ft.padding.symmetric(254                                            horizontal=10, vertical=4255                                        ),256                                    ),257                                ],258                                spacing=10,259                            ),260                            ft.Divider(height=1, color="#E2E8F0"),261                            # Preview image262                            ft.Container(263                                content=ft.Image(264                                    src=img_url,265                                    width=660,266                                    fit=ft.ImageFit.CONTAIN,267                                    border_radius=6,268                                ),269                                alignment=ft.alignment.center,270                            ),271                            ft.Divider(height=1, color="#E2E8F0"),272                            # Download button273                            ft.Row(274                                [275                                    ft.ElevatedButton(276                                        "Download Result",277                                        icon=ft.icons.DOWNLOAD_ROUNDED,278                                        on_click=lambda _, u=img_url, n=out_name: page.launch_url(279                                            create_download_helper(n, u)280                                        ),281                                        bgcolor="#4F46E5",282                                        color="#FFFFFF",283                                        height=40,284                                        style=ft.ButtonStyle(285                                            shape=ft.RoundedRectangleBorder(radius=8)286                                        ),287                                    )288                                ],289                                alignment=ft.MainAxisAlignment.END,290                            ),291                        ],292                        spacing=14,293                    ),294                    padding=20,295                    width=700,296                ),297                elevation=2,298                surface_tint_color="#FFFFFF",299            )300            results_col.controls.append(card)301 302        page.update()303 304    # ── Progress / status ────────────────────────────305    status_label = ft.Text("", size=13, color="#64748B")306 307    # ── FilePicker ───────────────────────────────────308    file_picker = ft.FilePicker(309        on_result=lambda e: on_files_picked(e),310        on_upload=lambda e: on_upload_progress(e),311    )312    page.overlay.append(file_picker)313 314    def reset_session():315        selected_files.clear()316        pending_uploads.clear()317        upload_map.clear()318    319        file_list_col.controls.clear()320        results_col.controls.clear()321        log_col.controls.clear()322        log_box.visible = False323        status_label.value = ""324        start_btn.disabled = True325    326        for f in glob.glob(os.path.join(UPLOAD_DIR, "*")):327            if os.path.isfile(f):328                os.remove(f)329    330        for f in glob.glob(os.path.join(OUTPUT_DIR, "*")):331            if os.path.isfile(f):332                os.remove(f)333    334        zip_path = os.path.join(ASSETS_DIR, "all_results.zip")335        if os.path.exists(zip_path):336            os.remove(zip_path)337    338        page.update()339    340 341    def on_files_picked(e: ft.FilePickerResultEvent):342        if not e.files:343            return344    345        reset_session()346    347        upload_list = []348        for f in e.files:349            unique_name = f"{threading.get_ident()}_{f.name}"350            upload_map[f.name] = unique_name351            pending_uploads.add(unique_name)352            url = page.get_upload_url(unique_name, 600)353            upload_list.append(354                ft.FilePickerUploadFile(name=f.name, upload_url=url)355            )356    357        file_picker.upload(upload_list)358 359    def on_upload_progress(e: ft.FilePickerUploadEvent):360        if e.progress == 1.0:361            unique_name = upload_map.get(e.file_name, e.file_name)362            pending_uploads.discard(unique_name)363            fp = os.path.join(UPLOAD_DIR, unique_name)364            if fp not in selected_files:365                selected_files.append(fp)366            refresh_files()367 368    # ── Start button ─────────────────────────────────369    def on_start(_):370        if not selected_files:371            return372 373        start_btn.disabled = True374        clear_btn.disabled = True375        results_col.controls.clear()376        log_col.controls.clear()377        log_box.visible = False378        status_label.value = "Processing..."379        page.update()380 381        def run_all():382            total = len(selected_files)383            for idx, pdf_path in enumerate(selected_files, 1):384                pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]385                status_label.value = f"Processing {idx} of {total}: {pdf_name}"386                page.update()387                process_pdf(388                    pdf_path,389                    pdf_name,390                    log_fn=add_log,391                    done_fn=add_result_card,392                )393 394            all_pngs = glob.glob(os.path.join(OUTPUT_DIR, "*.png"))395            if all_pngs:396                zip_path = os.path.join(ASSETS_DIR, "all_results.zip")397                with zipfile.ZipFile(zip_path, "w") as zf:398                    for p in all_pngs:399                        zf.write(p, os.path.basename(p))400                results_col.controls.append(401                    ft.ElevatedButton(402                        "Download All Results",403                        icon=ft.icons.DOWNLOAD_FOR_OFFLINE_ROUNDED,404                        on_click=lambda _: page.launch_url("/all_results.zip"),405                        bgcolor="#059669",406                        color="#FFFFFF",407                        height=46,408                        style=ft.ButtonStyle(shape=ft.RoundedRectangleBorder(radius=10)),409                    )410                )411                page.update()412 413            414            start_btn.disabled = False415            clear_btn.disabled = False416            status_label.value = (417                f"✓  Inspection complete — {total} document(s) processed."418            )419            page.update()420 421        threading.Thread(target=run_all, daemon=True).start()422 423    start_btn = ft.ElevatedButton(424        "Start Inspection",425        icon=ft.icons.PLAY_ARROW_ROUNDED,426        on_click=on_start,427        disabled=True,428        bgcolor="#4F46E5",429        color="#FFFFFF",430        height=46,431        style=ft.ButtonStyle(shape=ft.RoundedRectangleBorder(radius=10)),432    )433 434    # ── Clear button ──────────────────────────────────435    def on_clear(_):436        selected_files.clear()437        pending_uploads.clear()438        file_list_col.controls.clear()439        results_col.controls.clear()440        log_col.controls.clear()441        log_box.visible = False442        status_label.value = ""443        start_btn.disabled = True444        page.update()445 446    clear_btn = ft.TextButton(447        "Clear all",448        on_click=on_clear,449        style=ft.ButtonStyle(color="#94A3B8"),450    )451 452    # ── Upload zone ───────────────────────────────────453    upload_zone = ft.Container(454        content=ft.Column(455            [456                ft.Icon(ft.icons.CLOUD_UPLOAD_ROUNDED, size=52, color="#4F46E5"),457                ft.Text(458                    "Click to upload PDF files",459                    size=15,460                    weight=ft.FontWeight.W_600,461                    color="#1E293B",462                ),463                ft.Text(464                    "Multiple documents supported — PDF only",465                    size=12,466                    color="#94A3B8",467                ),468            ],469            horizontal_alignment=ft.CrossAxisAlignment.CENTER,470            spacing=8,471        ),472        width=700,473        border=ft.border.all(2, "#C7D2FE"),474        border_radius=14,475        bgcolor="#EEF2FF",476        padding=ft.padding.symmetric(vertical=40),477        alignment=ft.alignment.center,478        on_click=lambda _: file_picker.pick_files(479            allowed_extensions=["pdf"],480            allow_multiple=True,481        ),482        ink=True,483    )484 485    # ─────────────────────────────────────────────────486    # PAGE LAYOUT487    # ─────────────────────────────────────────────────488    page.add(489        ft.Column(490            [491                # ── Header ──492                ft.Container(height=8),493                ft.Text(494                    "PDF Layout Inspector",495                    size=32,496                    weight=ft.FontWeight.BOLD,497                    color="#1E293B",498                    text_align=ft.TextAlign.CENTER,499                ),500                ft.Text(501                    "Detect text overlapping structural lines in PDF documents",502                    size=14,503                    color="#64748B",504                    text_align=ft.TextAlign.CENTER,505                ),506                ft.Container(height=6),507                ft.Divider(color="#E2E8F0", height=1),508                ft.Container(height=14),509 510                # ── Upload ──511                upload_zone,512                ft.Container(height=12),513                file_list_col,514                upload_counter,515                ft.Row(516                    [clear_btn],517                    width=700,518                    alignment=ft.MainAxisAlignment.END,519                ),520                ft.Container(height=10),521 522                # ── Action row ──523                ft.Row(524                    [start_btn, status_label],525                    spacing=18,526                    alignment=ft.MainAxisAlignment.CENTER,527                    width=700,528                ),529                ft.Container(height=10),530 531                # ── Log ──532                log_box,533                ft.Container(height=24),534 535                # ── Results ──536                results_col,537                ft.Container(height=50),538            ],539            horizontal_alignment=ft.CrossAxisAlignment.CENTER,540            spacing=0,541        )542    )543 544 545# ─────────────────────────────────────────────546# ENTRY POINT547# ─────────────────────────────────────────────548ft.app(549    target=main,550    view=ft.AppView.WEB_BROWSER,551    port=7860,552    upload_dir=UPLOAD_DIR,553    assets_dir=ASSETS_DIR,554)