Team Ai
Apppublic

Maverick9876/document-extraction-workbench-cloud

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py545 linesDownload Raw Back to root
1import csv2import os3import shutil4import threading5import time6import queue7from contextlib import contextmanager8from pathlib import Path9 10import gradio as gr11 12from ai_provider import DEFAULT_GEMINI_MODEL, DEFAULT_OPENAI_MODEL13from credential_extractor import run_certificate_extraction14from main_sharepoint_ocr import run_sharepoint_ocr15from name_matcherv2 import MATCH_THRESHOLD, run_name_matching16 17 18APP_DIR = Path(__file__).resolve().parent19ARTIFACTS_DIR = APP_DIR / "artifacts"20ARTIFACTS_DIR.mkdir(exist_ok=True)21RUN_LOCK = threading.Lock()22ARTIFACT_RETENTION_SEC = int(os.getenv("ARTIFACT_RETENTION_SEC", str(6 * 60 * 60)))23 24CSS_FILE = APP_DIR / "styles.css"25CSS_CONTENT = CSS_FILE.read_text(encoding="utf-8") if CSS_FILE.exists() else ""26 27APP_THEME = gr.themes.Base(28    primary_hue=gr.themes.colors.emerald,29    secondary_hue=gr.themes.colors.cyan,30    neutral_hue=gr.themes.colors.slate,31    font=[gr.themes.GoogleFont("Plus Jakarta Sans"), "ui-sans-serif", "system-ui", "sans-serif"],32    font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "ui-monospace", "Consolas", "monospace"],33).set(34    body_background_fill="#f3f7fb",35    body_background_fill_dark="#09121f",36    body_text_color="#102033",37    body_text_color_dark="#e9f1fb",38    body_text_color_subdued="#607286",39    body_text_color_subdued_dark="#95a9bf",40    background_fill_primary="#f3f7fb",41    background_fill_primary_dark="#09121f",42    background_fill_secondary="#ffffff",43    background_fill_secondary_dark="#101b2d",44    block_background_fill="#ffffff",45    block_background_fill_dark="#101b2d",46    block_border_color="#d8e3ef",47    block_border_color_dark="#23364e",48    block_label_background_fill="transparent",49    block_label_background_fill_dark="transparent",50    block_label_text_color="#102033",51    block_label_text_color_dark="#dbe8f7",52    block_title_text_color="#102033",53    block_title_text_color_dark="#f1f6ff",54    button_primary_background_fill="#11715d",55    button_primary_background_fill_dark="#29c6a2",56    button_primary_background_fill_hover="#0d5a4a",57    button_primary_background_fill_hover_dark="#1fa98a",58    button_primary_text_color="#ffffff",59    button_primary_text_color_dark="#05211b",60    button_secondary_background_fill="#eef5fb",61    button_secondary_background_fill_dark="#162438",62    button_secondary_background_fill_hover="#e2edf8",63    button_secondary_background_fill_hover_dark="#20324b",64    button_secondary_text_color="#17304d",65    button_secondary_text_color_dark="#d7e6f8",66    input_background_fill="#fdfefe",67    input_background_fill_dark="#0d1727",68    input_border_color="#c7d7e8",69    input_border_color_dark="#30445f",70    border_color_primary="#c7d7e8",71    border_color_primary_dark="#30445f",72    link_text_color="#11715d",73    link_text_color_dark="#56dfc0",74    link_text_color_hover="#0d5a4a",75    link_text_color_hover_dark="#83eed5",76    block_radius="22px",77    button_large_radius="999px",78    input_radius="16px",79)80 81 82def _timestamp() -> str:83    return time.strftime("%Y%m%d_%H%M%S")84 85 86def _default_model_for_provider(provider: str) -> str:87    return DEFAULT_GEMINI_MODEL if (provider or "").strip().lower() == "gemini" else DEFAULT_OPENAI_MODEL88 89 90def _provider_ui_defaults(provider: str):91    provider = (provider or "openai").strip().lower()92    if provider == "gemini":93        default_model = DEFAULT_GEMINI_MODEL94        note = (95            "**Gemini selected.** Use a Gemini API key and keep the default model unless you need a specific "96            "vision-capable variant."97        )98    else:99        default_model = DEFAULT_OPENAI_MODEL100        note = (101            "**OpenAI selected.** This is the default path for SharePoint OCR and certificate AI fallback. "102            "Faster models can help on large batches, but smaller CSV runs are still the smoothest on free CPU hosting."103        )104    return gr.update(value=default_model, placeholder=f"Default: {default_model}"), note, gr.update(value="")105 106 107def _run_dir(prefix: str) -> Path:108    run_dir = ARTIFACTS_DIR / f"{prefix}_{_timestamp()}_{os.getpid()}"109    run_dir.mkdir(parents=True, exist_ok=True)110    return run_dir111 112 113def _write_sample_csv(path: Path, rows: list[list[str]]) -> str:114    with path.open("w", encoding="utf-8", newline="") as handle:115        writer = csv.writer(handle)116        writer.writerows(rows)117    return str(path)118 119 120def build_sample_ocr_csv() -> str:121    return _write_sample_csv(122        ARTIFACTS_DIR / "sharepoint_ocr_sample.csv",123        [124            ["link"],125            ["https://contoso.sharepoint.com/sites/hr/Shared%20Documents/passport_sample.pdf"],126            ["https://contoso.sharepoint.com/sites/hr/Shared%20Documents/id_card_sample.png"],127        ],128    )129 130 131def build_sample_cert_csv() -> str:132    return _write_sample_csv(133        ARTIFACTS_DIR / "certificate_extraction_sample.csv",134        [135            ["link"],136            ["https://www.credential.net/example-certificate-link-1"],137            ["https://www.credential.net/example-certificate-link-2"],138        ],139    )140 141 142def build_sample_match_csv() -> str:143    return _write_sample_csv(144        ARTIFACTS_DIR / "name_matching_sample.csv",145        [146            ["Email", "Name Entered", "Extracted Database"],147            ["alex@example.com", "Alex Johnson", "Alexander Johnson"],148            ["priya@example.com", "Priya Sharma", "Priya R Sharma"],149            ["omar@example.com", "Omar Khan", "Omar A Khan"],150        ],151    )152 153 154def cleanup_old_artifacts() -> None:155    cutoff = time.time() - ARTIFACT_RETENTION_SEC156    for path in ARTIFACTS_DIR.iterdir():157        try:158            if path.is_dir() and path.stat().st_mtime < cutoff:159                shutil.rmtree(path, ignore_errors=True)160            elif path.is_file() and path.stat().st_mtime < cutoff:161                path.unlink(missing_ok=True)162        except Exception:163            pass164 165 166@contextmanager167def single_run_guard():168    acquired = RUN_LOCK.acquire(blocking=False)169    if not acquired:170        raise gr.Error("Another job is already running in this Space. Wait for it to finish, then try again.")171    try:172        yield173    finally:174        RUN_LOCK.release()175 176 177@contextmanager178def configured_ai_env(provider: str, api_key: str, model: str):179    provider = (provider or "openai").strip().lower()180    if provider not in {"openai", "gemini"}:181        raise ValueError("Provider must be either 'openai' or 'gemini'.")182 183    previous = {184        "AI_PROVIDER": os.environ.get("AI_PROVIDER"),185        "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY"),186        "OPENAI_MODEL": os.environ.get("OPENAI_MODEL"),187        "GEMINI_API_KEY": os.environ.get("GEMINI_API_KEY"),188        "GEMINI_MODEL": os.environ.get("GEMINI_MODEL"),189    }190 191    os.environ["AI_PROVIDER"] = provider192    os.environ.pop("OPENAI_API_KEY", None)193    os.environ.pop("GEMINI_API_KEY", None)194 195    if provider == "openai":196        os.environ["OPENAI_MODEL"] = (model or DEFAULT_OPENAI_MODEL).strip() or DEFAULT_OPENAI_MODEL197        if api_key.strip():198            os.environ["OPENAI_API_KEY"] = api_key.strip()199    else:200        os.environ["GEMINI_MODEL"] = (model or DEFAULT_GEMINI_MODEL).strip() or DEFAULT_GEMINI_MODEL201        if api_key.strip():202            os.environ["GEMINI_API_KEY"] = api_key.strip()203 204    try:205        yield206    finally:207        for key, value in previous.items():208            if value is None:209                os.environ.pop(key, None)210            else:211                os.environ[key] = value212 213 214def _copy_uploaded_csv(src_path: str, target_dir: Path, target_name: str) -> Path:215    if not src_path:216        raise ValueError("Please upload a CSV file first.")217    src = Path(src_path)218    if not src.exists():219        raise ValueError("Uploaded CSV file could not be found on the server.")220    dst = target_dir / target_name221    shutil.copy2(src, dst)222    return dst223 224 225def _friendly_error(exc: Exception) -> gr.Error:226    message = str(exc).strip() or exc.__class__.__name__227    lowered = message.lower()228 229    if "429" in lowered or "rate limit" in lowered:230        message = "The provider rate limit was hit. Retry in a minute or use a lighter model."231    elif "api key" in lowered and "not set" in lowered:232        message = "Add a valid API key to run this workflow."233    elif "playwright" in lowered or "browser" in lowered:234        message = "Browser automation failed on the hosted runtime. Retry with a smaller CSV or try again shortly."235    elif "csv must include a header column named 'link'" in lowered:236        message = "The uploaded OCR CSV must contain a header column named 'link'."237    elif "missing required columns" in lowered:238        message = "The name matching CSV must include Email, Name Entered, and Extracted Database columns."239    elif "timed out" in lowered or "timeout" in lowered:240        message = "The hosted job timed out. Try a smaller file or fewer large PDFs per run."241 242    return gr.Error(message)243 244 245def run_sharepoint_workflow(csv_file: str, provider: str, api_key: str, model: str):246    if not api_key.strip():247        raise gr.Error("Add your API key to run SharePoint OCR.")248 249    with single_run_guard():250        cleanup_old_artifacts()251        run_dir = _run_dir("sharepoint")252        input_csv = _copy_uploaded_csv(csv_file, run_dir, "sharepoint_input.csv")253        output_dir = run_dir / "outputs"254        output_dir.mkdir(exist_ok=True)255 256        logs = []257        log_queue = queue.Queue()258 259        def status_callback(message: str):260            log_queue.put(str(message))261 262        def progress_callback(done: int, total: int):263            log_queue.put(f"Progress: {done} of {total}")264 265        result_holder = {}266 267        def thread_target():268            try:269                with configured_ai_env(270                    provider=provider,271                    api_key=api_key,272                    model=(model or _default_model_for_provider(provider)),273                ):274                    output_path = run_sharepoint_ocr(275                        csv_path=str(input_csv),276                        output_dir=str(output_dir),277                        api_key=api_key.strip(),278                        allow_api_key_prompt=False,279                        progress_callback=progress_callback,280                        status_callback=status_callback,281                        show_progress=False,282                    )283                    result_holder["output_path"] = output_path284            except Exception as exc:285                result_holder["error"] = exc286            finally:287                log_queue.put(None)288 289        t = threading.Thread(target=thread_target)290        t.start()291 292        while t.is_alive() or not log_queue.empty():293            try:294                msg = log_queue.get(timeout=0.2)295                if msg is None:296                    break297                logs.append(msg)298                yield None, "\n".join(logs[-25:])299            except queue.Empty:300                pass301 302        t.join()303        if "error" in result_holder:304            raise _friendly_error(result_holder["error"])305 306        output_path = result_holder.get("output_path")307        yield str(output_path), "\n".join(logs[-25:]) or "SharePoint OCR completed."308 309 310def run_certificate_workflow(csv_file: str, provider: str, api_key: str, model: str, use_ai: bool):311    if use_ai and not api_key.strip():312        raise gr.Error("Add your API key or turn off AI fallback for certificate extraction.")313 314    with single_run_guard():315        cleanup_old_artifacts()316        run_dir = _run_dir("certificate")317        input_csv = _copy_uploaded_csv(csv_file, run_dir, "certificate_input.csv")318        output_csv = run_dir / "output_with_certificate_details.csv"319 320        logs = []321        log_queue = queue.Queue()322 323        def status_callback(message: str):324            log_queue.put(str(message))325 326        def progress_callback(done: int, total: int):327            log_queue.put(f"Progress: {done} of {total}")328 329        result_holder = {}330 331        def thread_target():332            try:333                with configured_ai_env(334                    provider=provider,335                    api_key=api_key,336                    model=(model or _default_model_for_provider(provider)),337                ):338                    output_path = run_certificate_extraction(339                        input_csv=str(input_csv),340                        output_csv=str(output_csv),341                        progress_callback=progress_callback,342                        status_callback=status_callback,343                        use_openai_ocr=bool(use_ai),344                        openai_api_key=api_key.strip() or None,345                    )346                    result_holder["output_path"] = output_path347            except Exception as exc:348                result_holder["error"] = exc349            finally:350                log_queue.put(None)351 352        t = threading.Thread(target=thread_target)353        t.start()354 355        while t.is_alive() or not log_queue.empty():356            try:357                msg = log_queue.get(timeout=0.2)358                if msg is None:359                    break360                logs.append(msg)361                yield None, "\n".join(logs[-25:])362            except queue.Empty:363                pass364 365        t.join()366        if "error" in result_holder:367            raise _friendly_error(result_holder["error"])368 369        output_path = result_holder.get("output_path")370        yield str(output_path), "\n".join(logs[-25:]) or "Certificate extraction completed."371 372 373def run_name_matching_workflow(csv_file: str, threshold: float):374    try:375        cleanup_old_artifacts()376        with single_run_guard():377            run_dir = _run_dir("matching")378            input_csv = _copy_uploaded_csv(csv_file, run_dir, "name_matching_input.csv")379            output_csv = run_dir / "matched_output.csv"380            output_path = run_name_matching(381                csv_file=str(input_csv),382                output_file=str(output_csv),383                threshold=float(threshold or MATCH_THRESHOLD),384            )385    except gr.Error:386        raise387    except Exception as exc:388        raise _friendly_error(exc)389 390    return str(output_path), f"Name matching completed with threshold {float(threshold or MATCH_THRESHOLD):.2f}."391 392 393with gr.Blocks(394    title="Document Extraction Workbench Cloud",395    theme=APP_THEME,396    css=CSS_CONTENT,397    fill_width=True,398    delete_cache=(3600, 3600 * 6),399) as demo:400    gr.HTML(401        """402        <header class="app-header">403          <div class="header-main">404            <div class="header-logo">405              <svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">406                <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" fill="currentColor" fill-opacity="0.15"/>407                <path d="M14 2H6C4.9 2 4.01 2.9 4.01 4L4 20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2ZM18 20H6V4H13V9H18V20ZM9 16H15V18H9V16ZM9 12H15V14H9V12ZM9 8H11V10H9V8Z" fill="currentColor"/>408              </svg>409              <h1>Document Extraction Workbench</h1>410            </div>411            <div class="header-badge">Cloud Edition</div>412          </div>413        </header>414        """415    )416 417    with gr.Column(elem_classes=["config-panel"]):418        with gr.Row():419            provider = gr.Dropdown(420                choices=["openai", "gemini"],421                value="openai",422                label="AI Provider",423            )424            model = gr.Textbox(425                label="AI Model",426                value=DEFAULT_OPENAI_MODEL,427                placeholder=f"Default: {DEFAULT_OPENAI_MODEL}",428            )429            api_key = gr.Textbox(430                label="API Key",431                type="password",432                placeholder="Enter API key for session...",433            )434        provider_note = gr.Markdown(435            "**OpenAI selected.** Default path for SharePoint OCR and certificate AI fallback.",436            elem_classes=["provider-note"],437        )438 439    with gr.Tabs(elem_classes=["tool-tabs"]):440        with gr.Tab("SharePoint OCR"):441            with gr.Row(equal_height=True):442                with gr.Column(elem_classes=["panel-card"]):443                    sharepoint_input = gr.File(444                        label="Upload Input CSV (must include 'link' header)",445                        file_types=[".csv"],446                        type="filepath",447                    )448                    with gr.Row():449                        sharepoint_sample = gr.File(450                            value=build_sample_ocr_csv,451                            label="Template CSV",452                            interactive=False,453                        )454                    sharepoint_run = gr.Button("Run SharePoint OCR", variant="primary", elem_classes=["action-btn"])455                with gr.Column(elem_classes=["panel-card"]):456                    sharepoint_output = gr.File(label="Result CSV")457                    sharepoint_status = gr.Textbox(label="Activity Feed / Logs", lines=12, elem_classes=["log-box"])458            sharepoint_run.click(459                run_sharepoint_workflow,460                inputs=[sharepoint_input, provider, api_key, model],461                outputs=[sharepoint_output, sharepoint_status],462                concurrency_limit=1,463            )464 465        with gr.Tab("Certificate Extraction"):466            with gr.Row(equal_height=True):467                with gr.Column(elem_classes=["panel-card"]):468                    cert_input = gr.File(469                        label="Upload Input CSV (must include certificate links)",470                        file_types=[".csv"],471                        type="filepath",472                    )473                    cert_use_ai = gr.Checkbox(474                        value=True,475                        label="Enable AI OCR fallback for certificate images",476                    )477                    with gr.Row():478                        cert_sample = gr.File(479                            value=build_sample_cert_csv,480                            label="Template CSV",481                            interactive=False,482                        )483                    cert_run = gr.Button("Run Certificate Extraction", variant="primary", elem_classes=["action-btn"])484                with gr.Column(elem_classes=["panel-card"]):485                    cert_output = gr.File(label="Result CSV")486                    cert_status = gr.Textbox(label="Activity Feed / Logs", lines=12, elem_classes=["log-box"])487            cert_run.click(488                run_certificate_workflow,489                inputs=[cert_input, provider, api_key, model, cert_use_ai],490                outputs=[cert_output, cert_status],491                concurrency_limit=1,492            )493 494        with gr.Tab("Name Matching"):495            with gr.Row(equal_height=True):496                with gr.Column(elem_classes=["panel-card"]):497                    match_input = gr.File(498                        label="Upload Input CSV (Email, Name Entered, Extracted Database headers)",499                        file_types=[".csv"],500                        type="filepath",501                    )502                    match_threshold = gr.Slider(503                        value=MATCH_THRESHOLD,504                        label="Match Threshold",505                        minimum=0.0,506                        maximum=1.0,507                        step=0.05,508                    )509                    with gr.Row():510                        match_sample = gr.File(511                            value=build_sample_match_csv,512                            label="Template CSV",513                            interactive=False,514                        )515                    match_run = gr.Button("Run Name Matching", variant="primary", elem_classes=["action-btn"])516                with gr.Column(elem_classes=["panel-card"]):517                    match_output = gr.File(label="Result CSV")518                    match_status = gr.Textbox(label="Activity Feed / Logs", lines=12, elem_classes=["log-box"])519            match_run.click(520                run_name_matching_workflow,521                inputs=[match_input, match_threshold],522                outputs=[match_output, match_status],523                concurrency_limit=1,524            )525 526    gr.HTML(527        """528        <div class="app-footnote">529          Document Extraction Workbench - Dedicated End-User Interface530        </div>531        """532    )533 534    provider.change(535        fn=_provider_ui_defaults,536        inputs=provider,537        outputs=[model, provider_note, api_key],538        queue=False,539    )540 541demo.queue(default_concurrency_limit=1)542 543if __name__ == "__main__":544    demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))545