Spaces:
Running on Zero
Running on Zero
Download app.py from Markuspierre/Decompte: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/spaces/Markuspierre/Decompte/resolve/main/app.py
- Command line
-
hf download hf://spaces/Markuspierre/Decompte/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Markuspierre/Decompte/resolve/main/app.py
11.7 kB
| import os | |
| import tempfile | |
| import gradio as gr | |
| import pandas as pd | |
| import pytesseract | |
| import ocr_core as oc | |
| try: | |
| import spaces | |
| 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 = ('<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" ' | |
| 'stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V5a1 1 0 0 1 1-1h2M17 4h2a1 1 0 0 1 1 1v2' | |
| 'M20 17v2a1 1 0 0 1-1 1h-2M7 20H5a1 1 0 0 1-1-1v-2"/><path d="M8 9h8M8 12h8M8 15h5"/></svg>') | |
| 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""" | |
| <div class="top"> | |
| <div class="brand"> | |
| <div class="logo">{LOGO}</div> | |
| <div> | |
| <h1>OCR COUNT</h1> | |
| <p>Reconnaissance et décompte automatique de tableaux photographiés</p> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| 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(""" | |
| <div class="card-head"><b>Importer les images</b></div> | |
| """) | |
| 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(""" | |
| <div class="card" style="margin-top:16px"> | |
| <div class="card-head"><b>Catégories comptées</b></div> | |
| <div class="chips"> | |
| <span class="chip" style="--c:#3b82f6"><i></i>PASF</span> | |
| <span class="chip" style="--c:#14b8a6"><i></i>IPM / ASS</span> | |
| <span class="chip" style="--c:#8b5cf6"><i></i>PR</span> | |
| <span class="chip" style="--c:#f59e0b"><i></i>MUT</span> | |
| <span class="chip" style="--c:#ec4899"><i></i>RESULTAT</span> | |
| <span class="chip" style="--c:#22c55e"><i></i>CONTROL</span> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Column(scale=7, min_width=420): | |
| with gr.Column(elem_classes="card"): | |
| gr.HTML(""" | |
| <div class="card-head"><b>Rapport du décompte</b></div> | |
| """) | |
| 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(""" | |
| <div class="card-head"><b>Exporter</b></div> | |
| <p class="card-sub">Téléchargez le rapport au format Excel ou CSV.</p> | |
| """) | |
| 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) | |