Decompte / app.py
Markuspierre's picture
Update app.py
35939a3 verified
Raw History Blame Contribute Delete
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
@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 = ('<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)