import os import tempfile import gradio as gr import pandas as pd import pytesseract import ocr_core as oc try: import spaces @spaces.GPU def _gpu_placeholder(): return None except Exception: pass COLS = oc.COLS # ========================================================= # STYLE CSS (refonte visuelle uniquement) # ========================================================= CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); :root{ --bg:#f5f6fa; --card:#ffffff; --tx:#0f172a; --mu:#64748b; --bd:#e4e7ef; --ac:#4f46e5; --ac2:#eef2ff; --ok:#16a34a; --sh:0 1px 2px rgba(15,23,42,.04), 0 10px 30px rgba(15,23,42,.06); } .dark{ --bg:#0b1020; --card:#131a2e; --tx:#e8ecf7; --mu:#94a0bd; --bd:#242d49; --ac:#818cf8; --ac2:#1b2342; --sh:0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.35); } body, .gradio-container{ background:var(--bg) !important; color:var(--tx); font-family:'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif !important; } .gradio-container{ max-width:1240px !important; margin:0 auto !important; padding:0 18px 48px !important; } footer{ display:none !important; } /* ---------- EN-TÊTE ---------- */ .top{ display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; padding:24px 4px 18px; } .brand{ display:flex; align-items:center; gap:14px; } .logo{ width:46px; height:46px; border-radius:14px; display:grid; place-items:center; background:linear-gradient(135deg,#6366f1,#3b82f6); box-shadow:0 8px 20px rgba(79,70,229,.35); } .brand h1{ margin:0 !important; font-size:22px !important; font-weight:800 !important; letter-spacing:-.02em; color:var(--tx) !important; } .brand p{ margin:2px 0 0 !important; color:var(--mu) !important; font-size:13px; } .tags{ display:flex; gap:8px; flex-wrap:wrap; } .tag{ font-size:12px; font-weight:600; color:var(--ac); background:var(--ac2); border:1px solid var(--bd); padding:6px 12px; border-radius:999px; } /* ---------- CARTES ---------- */ .card{ background:var(--card) !important; border:1px solid var(--bd) !important; border-radius:20px !important; box-shadow:var(--sh); padding:22px !important; } .card-head{ display:flex; align-items:center; gap:10px; margin-bottom:4px; } .num{ width:28px; height:28px; border-radius:50%; background:var(--ac); color:#fff; font-weight:700; font-size:13px; display:grid; place-items:center; flex:none; } .card-head b{ font-size:16px; color:var(--tx); } .card-sub{ color:var(--mu); font-size:13px; line-height:1.55; margin:0 0 14px 38px; } .card .block, .card .html-container, .card .prose{ background:transparent !important; border:0 !important; box-shadow:none !important; } .card{ gap:6px !important; } /* ---------- ZONES FICHIERS ---------- */ #drop, #exp{ border:2px dashed var(--bd) !important; border-radius:16px !important; background:var(--ac2) !important; box-shadow:none !important; } #drop{ min-height:190px; } #drop button, #drop .upload-container, #exp button{ border:0 !important; background:transparent !important; box-shadow:none !important; } #drop *, #exp *{ --block-border-color:transparent; } /* ---------- BOUTONS ---------- */ #go{ background:linear-gradient(135deg,#6366f1,#3b82f6) !important; color:#fff !important; border:0 !important; border-radius:14px !important; min-height:54px !important; font-size:15px !important; font-weight:700 !important; letter-spacing:.01em; box-shadow:0 10px 24px rgba(79,70,229,.35) !important; transition:transform .15s ease, box-shadow .15s ease !important; } #go:hover{ transform:translateY(-1px); box-shadow:0 14px 30px rgba(79,70,229,.45) !important; } .ghost{ border-radius:12px !important; font-weight:600 !important; background:var(--ac2) !important; color:var(--ac) !important; border:1px solid var(--bd) !important; } /* ---------- RÉSULTATS ---------- */ .empty{ border:1px dashed var(--bd); border-radius:14px; padding:14px 16px; margin-bottom:12px; color:var(--mu); font-size:13px; background:var(--ac2); } .chips{ display:flex; flex-wrap:wrap; gap:8px; margin-top:6px; } .chip{ display:inline-flex; align-items:center; gap:7px; padding:7px 12px; border-radius:10px; background:var(--card); border:1px solid var(--bd); font-weight:600; font-size:12.5px; color:var(--tx); } .chip i{ width:9px; height:9px; border-radius:50%; background:var(--c); display:block; } .note{ color:var(--mu); font-size:12.5px; line-height:1.6; margin-top:12px; } .foot{ text-align:center; color:var(--mu); font-size:12px; padding:26px 0 0; } """ # ========================================================= # TRAITEMENT ET EXPORT (inchangé) # ========================================================= def build_outputs(df): df = df[df["Image"].astype(str).str.upper() != "TOTAL"].copy() for c in COLS: df[c] = pd.to_numeric( df[c], errors="coerce" ).fillna(0).astype(int) total = { "Image": "TOTAL", **{c: int(df[c].sum()) for c in COLS} } report = pd.concat( [ df[["Image"] + COLS], pd.DataFrame([total]) ], ignore_index=True ) folder = tempfile.mkdtemp() csv_path = os.path.join(folder, "decompte.csv") xlsx_path = os.path.join(folder, "decompte.xlsx") report.to_csv( csv_path, index=False, sep=";", encoding="utf-8-sig" ) report.to_excel( xlsx_path, index=False, sheet_name="Décompte" ) return report, [csv_path, xlsx_path] # ========================================================= # ANALYSE OCR (inchangé) # ========================================================= def run(files, lang, psm, fuzzy): if not files: raise gr.Error("Veuillez importer au moins une image.") rows = [] for f in files: path = f if isinstance(f, str) else f.name name = os.path.basename(path) try: counts, notes = oc.analyse( path, lang=lang, psm=int(psm), fuzzy=fuzzy ) except pytesseract.TesseractNotFoundError: raise gr.Error( "Tesseract est introuvable sur le serveur." ) except Exception as e: rows.append({ "Image": name, **{c: 0 for c in COLS} }) continue rows.append({ "Image": name, **counts }) report, out = build_outputs(pd.DataFrame(rows)) return report, out # ========================================================= # RECALCUL (inchangé) # ========================================================= def recompute(df): if df is None: raise gr.Error("Aucun tableau à recalculer.") return build_outputs(pd.DataFrame(df)) # ========================================================= # INTERFACE # ========================================================= LOGO = ('') with gr.Blocks( title="OCR COUNT | Décompte intelligent", theme=gr.themes.Soft( primary_hue="indigo", secondary_hue="slate", neutral_hue="slate" ), css=CSS ) as demo: # EN-TÊTE gr.HTML(f"""

OCR COUNT

Reconnaissance et décompte automatique de tableaux photographiés

""") with gr.Row(equal_height=False): # ---------- COLONNE GAUCHE : IMPORT + RÉGLAGES ---------- with gr.Column(scale=4, min_width=320): with gr.Column(elem_classes="card"): gr.HTML("""
Importer les images
""") files = gr.File( label="Déposez vos images ici ou cliquez pour parcourir", file_count="multiple", file_types=["image"], type="filepath", elem_id="drop" ) with gr.Accordion("Réglages de lecture (avancé)", open=False): with gr.Row(): lang = gr.Textbox( value="eng", label="Langue OCR", info="Langue utilisée par Tesseract" ) psm = gr.Dropdown( choices=[11, 6, 4], value=11, label="Mode de segmentation", info="Mode de lecture du tableau" ) fuzzy = gr.Checkbox( value=True, label="Tolérer les erreurs de lecture", info="Permet de reconnaître certains codes mal interprétés." ) go = gr.Button("Lancer le décompte", elem_id="go") gr.HTML("""
Catégories comptées
PASF IPM / ASS PR MUT RESULTAT CONTROL
""") with gr.Column(scale=7, min_width=420): with gr.Column(elem_classes="card"): gr.HTML("""
Rapport du décompte
""") table = gr.Dataframe( headers=["Image"] + COLS, datatype=["str"] + ["number"] * 6 + ["str"], interactive=True, type="pandas", label="", wrap=True ) recalc = gr.Button( "Recalculer le total", variant="secondary", elem_classes="ghost" ) with gr.Column(elem_classes="card"): gr.HTML("""
Exporter

Téléchargez le rapport au format Excel ou CSV.

""") exports = gr.File( label="Fichiers générés", file_count="multiple", interactive=False, elem_id="exp" ) # ÉVÉNEMENTS (inchangés) go.click( fn=run, inputs=[files, lang, psm, fuzzy], outputs=[table, exports] ) recalc.click( fn=recompute, inputs=table, outputs=[table, exports] ) if __name__ == "__main__": demo.launch(ssr_mode=False)