yonilev commited on
Commit
c1ca482
Β·
verified Β·
1 Parent(s): fdcac18

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +307 -453
app.py CHANGED
@@ -1,37 +1,31 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- Text2Receipt β€” Hugging Face Gradio Space
4
- =========================================
5
  Pipeline:
6
- raw Hebrew note
7
- β†’ fine-tuned parser (gemma-2-2b-it + LoRA)
8
- β†’ agentic clarifier (+5% bonus)
9
- β†’ deterministic complete() (VAT Β· serial Β· allocation number)
10
- β†’ fiscal document display
11
- β†’ FAISS recommender (3 similar past receipts)
12
- Repos:
13
- Dataset : yonilev/Text2Receipt
14
- Model : yonilev/Text2Receipt-parser
15
- Space : yonilev/Text2Receipt
16
  """
17
- import os, json, random
18
- # ── Disable SSR BEFORE importing gradio ──────────────────────────────────────
19
- # On HF Spaces, Gradio runs in SSR mode by default. In Gradio 6.17 this breaks
20
- # value-passing from Textbox inputs (raw_text arrives empty). Force CSR.
21
- os.environ["GRADIO_SSR_MODE"] = "false"
22
  import numpy as np
23
  import gradio as gr
 
 
24
 
25
- DATASET_REPO = "yonilev/Text2Receipt"
26
- MODEL_REPO = "yonilev/Text2Receipt-parser"
27
- BASE_MODEL = os.environ.get("BASE_MODEL", "unsloth/gemma-2-2b-it")
 
 
28
 
29
- import t2r_core as core
 
 
30
 
31
- # ═════════════════════════════════════════════════════════════════════════════
32
- # 1. Lazy init
33
- # ═════════════════════════════════════════════════════════════════════════════
34
- _STATE: dict = {"ready": False}
35
 
36
  def _lazy_init():
37
  if _STATE["ready"]:
@@ -98,9 +92,9 @@ def _lazy_init():
98
  print(f"βœ… FAISS ready β€” {emb.shape[0]} vectors")
99
 
100
 
101
- # ═════════════════════════════════════════════════════════════════════════════
102
- # 2. Parser
103
- # ═════════════════════════════════════════════════════════════════════════════
104
  INSTRUCTION = (
105
  "אΧͺΧ” ΧžΧžΧ™Χ¨ Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ” Χ—Χ•Χ€Χ©Χ™Χͺ Χ‘Χ’Χ‘Χ¨Χ™Χͺ ΧœΧžΧ‘Χ Χ” JSON. "
106
  "Χ—ΧœΧ₯ אך Χ•Χ¨Χ§ אΧͺ ΧžΧ” Χ©Χ›ΧͺΧ•Χ‘ Χ‘Χ”Χ’Χ¨Χ”: שם Χ”ΧœΧ§Χ•Χ— (client_name), "
@@ -109,9 +103,18 @@ INSTRUCTION = (
109
  "Χ”Χ—Χ–Χ¨ JSON ΧͺΧ§Χ™ΧŸ Χ‘ΧœΧ‘Χ“, ללא טקבט Χ Χ•Χ‘Χ£."
110
  )
111
 
 
 
 
 
 
 
 
 
112
  def _build_prompt(raw_text):
113
  return f"{INSTRUCTION}\n\nΧ”Χ’Χ¨Χ”: {raw_text}\n\nJSON:"
114
 
 
115
  def _extract_json(text):
116
  s = text.find("{")
117
  if s < 0: return None
@@ -125,6 +128,37 @@ def _extract_json(text):
125
  except: return None
126
  return None
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  def model_parse(raw_text):
129
  tok, model = _STATE["tok"], _STATE["model"]
130
  msgs = [{"role": "user", "content": _build_prompt(raw_text)}]
@@ -135,15 +169,9 @@ def model_parse(raw_text):
135
  out = model.generate(**enc, max_new_tokens=128, do_sample=False,
136
  pad_token_id=tok.pad_token_id)
137
  decoded = tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
138
- return _extract_json(decoded)
139
-
140
 
141
- # ═════════════════════════════════════════════════════════════════════════════
142
- # 3. Agentic clarification
143
- # ═════════════════════════════════════════════════════════════════════════════
144
- import datetime as _dt
145
-
146
- def _today(): return _dt.date.today()
147
 
148
  def _missing_fields(parse):
149
  if parse is None: return ["parse_failed"]
@@ -152,50 +180,10 @@ def _missing_fields(parse):
152
  if not parse.get("items"): m.append("items")
153
  return m
154
 
155
- def _needs_allocation_clarification(parse):
156
- if not parse: return False
157
- subtotal = sum(it.get("unit_price", 0) * it.get("quantity", 1)
158
- for it in parse.get("items", []))
159
- threshold = core.allocation_threshold_for_date(_today())
160
- return bool(parse.get("client_is_business")) and subtotal >= threshold * 0.8
161
-
162
- def clarification_questions(parse, answers):
163
- missing = _missing_fields(parse)
164
- if "parse_failed" in missing:
165
- return ["לא Χ”Χ¦ΧœΧ—ΧͺΧ™ ΧœΧ—ΧœΧ₯ Χ€Χ¨Χ˜Χ™Χ. Χ Χ‘Χ— ΧžΧ—Χ“Χ© β€” ΧœΧ“Χ•Χ’ΧžΧ”: 'Χ§Χ™Χ‘ΧœΧͺΧ™ 500β‚ͺ מ[שם] גל [Χ©Χ™Χ¨Χ•Χͺ]'"]
166
- qs = []
167
- if "client_name" in missing and "client_name" not in answers:
168
- qs.append("ΧžΧ” שם Χ”ΧœΧ§Χ•Χ—?")
169
- if "items" in missing and "items" not in answers:
170
- qs.append("ΧžΧ” Χ”Χ©Χ™Χ¨Χ•Χͺ/Χ”ΧžΧ•Χ¦Χ¨ ובאיזה ΧžΧ—Χ™Χ¨?")
171
- if "doc_type" not in answers:
172
- if parse and _needs_allocation_clarification(parse):
173
- qs.append('האם ΧœΧ”Χ Χ€Χ™Χ§ Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב/Χ§Χ‘ΧœΧ” (לגבק גם מג"מ) או Χ§Χ‘ΧœΧ” Χ€Χ©Χ•Χ˜Χ”?')
174
- elif parse and not parse.get("doc_type"):
175
- qs.append("איזה מבמך? Χ§Χ‘ΧœΧ” / Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב / Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב Χ•Χ§Χ‘ΧœΧ”")
176
- return qs[:3]
177
-
178
- def apply_answers(parse, answers):
179
- p = dict(parse or {})
180
- if "client_name" in answers: p["client_name"] = answers["client_name"]
181
- if "doc_type" in answers:
182
- raw = answers["doc_type"].strip()
183
- p["doc_type"] = ("tax_invoice_receipt" if "מב/Χ§Χ‘ΧœΧ”" in raw or "מב Χ•Χ§Χ‘ΧœΧ”" in raw
184
- else "tax_invoice" if "מב" in raw else "receipt")
185
- p.setdefault("doc_type", "receipt")
186
- p.setdefault("date", _today().isoformat())
187
- p.setdefault("payment_method", "bank_transfer")
188
- p.setdefault("amount_basis", "net")
189
- p.setdefault("currency", "ILS")
190
- p.setdefault("client_tax_id", None)
191
- p.setdefault("client_is_business", False)
192
- p.setdefault("items", [])
193
- return p
194
-
195
-
196
- # ═════════════════════════════════════════════════════════════════════════════
197
- # 4. FAISS recommender
198
- # ═════════════════════════════════════════════════════════════════════════════
199
  def recommend(query_text, k=3):
200
  pref = "query: " if _STATE["e5_family"] else ""
201
  q_emb = _STATE["enc"].encode([pref + query_text],
@@ -210,9 +198,186 @@ def recommend(query_text, k=3):
210
  return out
211
 
212
 
213
- # ═════════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  # 5. HTML renderers
215
- # ═════════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  def render_document(c, show_vat=True):
217
  issuer = c.get("issuer", {})
218
  client = c.get("client", {})
@@ -233,6 +398,17 @@ def render_document(c, show_vat=True):
233
  f'</tr>'
234
  for ln in lines)
235
 
 
 
 
 
 
 
 
 
 
 
 
236
  return f"""
237
  <div dir="rtl" style="font-family:'Segoe UI','Arial Hebrew',Arial,sans-serif;
238
  background:#0f172a;color:#e2e8f0;border-radius:16px;
@@ -289,16 +465,7 @@ def render_document(c, show_vat=True):
289
  </table>
290
 
291
  <div style="background:#1e293b;border-radius:10px;padding:16px 18px;">
292
- {"" if not show_vat else f'''
293
- <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:8px;">
294
- <span>בכום ΧœΧ€Χ Χ™ מג&quot;מ</span>
295
- <span style="color:#94a3b8;">β‚ͺ{c.get("subtotal",0):,.2f}</span>
296
- </div>
297
- <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;
298
- padding-bottom:12px;border-bottom:1px solid #334155;margin-bottom:12px;">
299
- <span>מג&quot;מ {vat_pct}%</span>
300
- <span style="color:#94a3b8;">β‚ͺ{c.get("vat_amount",0):,.2f}</span>
301
- </div>'''}
302
  <div style="display:flex;justify-content:space-between;align-items:baseline;">
303
  <span style="font-size:15px;font-weight:600;color:#e2e8f0;">Χ‘Χ”"Χ› לΧͺΧ©ΧœΧ•Χ</span>
304
  <span style="font-size:24px;font-weight:700;color:#2dd4bf;">β‚ͺ{c.get("total",0):,.2f}</span>
@@ -333,253 +500,60 @@ def render_recommendations(recs):
333
  </div>"""
334
 
335
 
336
- # ═════════════════════════════════════════════════════════════════════════════
337
- # 6. Pipeline
338
- # ═════════════════════════════════════════════════════════════════════════════
339
- DEMO_ISSUER_BASE = {"name": "ΧžΧ™Χ Χ©Χ§Χ˜Χ™Χ", "tax_id": "962569844"}
340
-
341
- # ── VAT cue patterns ─────────────────────────────────────────────────────────
342
- import re as _re
343
-
344
- _GROSS_PATTERNS = [r"Χ›Χ•ΧœΧœ\s*מג[\"Χ΄]?מ", r"Χ›Χ•ΧœΧœ\s*מב", r"Χ’Χ¨Χ•Χ‘", r"Χ‘Χ¨Χ•Χ˜Χ•",
345
- r"ΧžΧ—Χ™Χ¨\s*Χ›Χ•ΧœΧœ"]
346
- _NET_PATTERNS = [r"ΧœΧ€Χ Χ™\s*מג[\"Χ΄]?מ", r"\+\s*מג[\"Χ΄]?מ", r"Χ‘ΧͺΧ•Χ‘Χ€Χͺ\s*מג[\"Χ΄]?מ",
347
- r"Χ€ΧœΧ•Χ‘\s*מג[\"Χ΄]?מ", r"Χ Χ˜Χ•", r"לא\s*Χ›Χ•ΧœΧœ\s*מג[\"Χ΄]?מ",
348
- r"Χ‘ΧœΧ™\s*מג[\"Χ΄]?מ", r"ΧœΧ€Χ Χ™\s*מב"]
349
- _EXEMPT_PATTERNS= [r"Χ’Χ•Χ‘Χ§\s*Χ€Χ˜Χ•Χ¨", r"Χ€Χ˜Χ•Χ¨\s*ממג[\"Χ΄]?מ", r"ללא\s*מג[\"Χ΄]?מ"]
350
- _PAYMENT_RECV = [r"Χ§Χ™Χ‘ΧœΧͺΧ™", r"Χ©Χ™ΧœΧž", r"Χ”Χ’Χ‘Χ™Χ¨", r"Χ©Χ•ΧœΧ", r"Χ Χ›Χ Χ‘", r"Χ”Χ•Χ’Χ‘Χ¨",
351
- r"Χ‘Χ™Χ˜", r"Χ€Χ™Χ™", r"ΧžΧ–Χ•ΧžΧŸ"]
352
-
353
- def _text_has(text, patterns):
354
- t = text.lower()
355
- return any(_re.search(p, t) for p in patterns)
356
-
357
- def _detect_date_from_text(text):
358
- """Try to extract a date from free text. Returns ISO string or None."""
359
- patterns = [
360
- (r"(\d{1,2})[/\-\.](\d{1,2})[/\-\.](\d{2,4})", "dmy"),
361
- (r"(\d{4})[/\-\.](\d{1,2})[/\-\.](\d{1,2})", "ymd"),
362
- ]
363
- for pat, order in patterns:
364
- m = _re.search(pat, text)
365
- if m:
366
- try:
367
- g = [int(x) for x in m.groups()]
368
- if order == "dmy":
369
- d, mo, y = g
370
- if y < 100: y += 2000
371
- else:
372
- y, mo, d = g
373
- return f"{y:04d}-{mo:02d}-{d:02d}"
374
- except Exception:
375
- pass
376
- return None
377
-
378
- def render_parse_panel(parse, completed, show_vat=True):
379
- """'Behind the scenes' panel β€” model extraction vs deterministic computation."""
380
- items = parse.get("items", [])
381
- item_rows = "".join(
382
- f'<div style="display:flex;justify-content:space-between;font-size:12px;'
383
- f'color:#334155;padding:3px 0;"><span>{it.get("description","")}</span>'
384
- f'<span style="color:#64748b;">{it.get("quantity",1)} Γ— β‚ͺ{it.get("unit_price",0):,.0f}</span></div>'
385
- for it in items)
386
-
387
- chips = (
388
- f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
389
- f'border-radius:999px;padding:2px 9px;font-size:11px;">ΧœΧ§Χ•Χ—: {parse.get("client_name","β€”")}</span>'
390
- f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
391
- f'border-radius:999px;padding:2px 9px;font-size:11px;">'
392
- f'{"Χ’Χ‘Χ§" if parse.get("client_is_business") else "Χ€Χ¨Χ˜Χ™"}</span>'
393
- f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
394
- f'border-radius:999px;padding:2px 9px;font-size:11px;">{len(items)} Χ€Χ¨Χ™Χ˜Χ™Χ</span>')
395
-
396
- alloc_line = ""
397
- if completed.get("allocation_required"):
398
- alloc_line = ('<div style="font-size:12px;color:#334155;padding:3px 0;">'
399
- 'מב׀ר הקצאה <span style="color:#0d9488;">βœ“ Χ Χ“Χ¨Χ©</span></div>')
400
-
401
- vat_line = (f'<div style="font-size:12px;color:#334155;padding:3px 0;">'
402
- f'מג"מ {int(round(completed.get("vat_rate",0)*100))}% · '
403
- f'β‚ͺ{completed.get("vat_amount",0):,.2f}</div>'
404
- if show_vat else
405
- '<div style="font-size:12px;color:#94a3b8;padding:3px 0;">ללא Χ€Χ™Χ¨Χ•Χ§ מג"מ (Χ§Χ‘ΧœΧ”)</div>')
406
-
407
- return f"""
408
- <div dir="rtl" style="font-family:'Segoe UI',Arial,sans-serif;max-width:660px;
409
- margin:14px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
410
- <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
411
- <div style="font-size:11px;font-weight:700;color:#0d9488;letter-spacing:.05em;
412
- text-transform:uppercase;margin-bottom:10px;">🧠 Χ”ΧžΧ•Χ“Χœ Χ—Χ™ΧœΧ₯</div>
413
- <div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px;">{chips}</div>
414
- {item_rows}
415
- <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
416
- padding-top:8px;">gemma-2-2b + LoRA Β· Χ—Χ•Χ–Χ” Χ¨Χ§ <code>parse</code></div>
417
- </div>
418
- <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
419
- <div style="font-size:11px;font-weight:700;color:#7c3aed;letter-spacing:.05em;
420
- text-transform:uppercase;margin-bottom:10px;">βš™οΈ Χ”ΧžΧ Χ•Χ’ Χ—Χ™Χ©Χ‘</div>
421
- {vat_line}
422
- <div style="font-size:12px;color:#334155;padding:3px 0;">Χ‘Χ”"Χ› β‚ͺ{completed.get("total",0):,.2f}</div>
423
- <div style="font-size:12px;color:#334155;padding:3px 0;">מב׳ מבמך {completed.get("serial_number","")}</div>
424
- {alloc_line}
425
- <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
426
- padding-top:8px;">Χ“Χ˜Χ¨ΧžΧ™Χ Χ™Χ‘Χ˜Χ™ Β· א׀ב Χ”Χ–Χ™Χ•Χͺ Χ‘Χ—Χ©Χ‘Χ•ΧŸ</div>
427
- </div>
428
- </div>"""
429
-
430
-
431
- def decide_fiscal(text, issuer_status, parse, explicit_doc_choice,
432
- clarify_vat_basis=None, clarify_client_tax_id=None):
433
- """Returns (issuer_dict, final_parse, questions_dict)."""
434
- today = _today()
435
-
436
- # ── 1. Issuer required first β€” no model run needed if missing ────────────
437
- if not issuer_status:
438
- return None, None, {"issuer_status": "Χ‘Χ—Χ¨ Χ‘Χ˜Χ˜Χ•Χ‘ ΧžΧ Χ€Χ™Χ§ ΧœΧžΧ’ΧœΧ” (Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨ / Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ”)."}
439
-
440
- questions = {}
441
- exempt = (issuer_status == "Χ€Χ˜Χ•Χ¨")
442
- issuer = {**DEMO_ISSUER_BASE,
443
- "status": "exempt_dealer" if exempt else "authorized_dealer",
444
- "is_company": False}
445
-
446
- # ── 2. Mandatory content ──────────────────────────────────────────────────
447
- if not parse.get("client_name", "").strip():
448
- questions["client_name"] = "שם Χ”ΧœΧ§Χ•Χ— / Χ”ΧžΧ©ΧœΧ?"
449
- if not parse.get("items"):
450
- questions["items"] = "ΧžΧ” Χ”Χ©Χ™Χ¨Χ•Χͺ/Χ”ΧžΧ•Χ¦Χ¨ ובאיזה בכום? (למשל: Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™ β‚ͺ500)"
451
-
452
- # ── 3. Date ───────────────────────────────────────────────────────────────
453
- parse = dict(parse)
454
- parse.setdefault("date", _detect_date_from_text(text) or today.isoformat())
455
-
456
- # ── 4. Doc type + VAT basis ───────────────────────────────────��───────────
457
- is_gross = _text_has(text, _GROSS_PATTERNS) or clarify_vat_basis == "Χ›Χ•ΧœΧœ מג״מ"
458
- is_net = _text_has(text, _NET_PATTERNS) or clarify_vat_basis == "ΧœΧ€Χ Χ™ מג״מ"
459
- has_vat_cue = is_gross or is_net
460
-
461
- if exempt:
462
- doc_type, amount_basis = "receipt", "net"
463
-
464
- elif explicit_doc_choice and explicit_doc_choice != "Χ–Χ™Χ”Χ•Χ™ ΧΧ•Χ˜Χ•ΧžΧ˜Χ™":
465
- doc_type = {"Χ§Χ‘ΧœΧ”": "receipt", "Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב": "tax_invoice",
466
- "Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב Χ•Χ§Χ‘ΧœΧ”": "tax_invoice_receipt"}.get(explicit_doc_choice, "receipt")
467
- if doc_type != "receipt":
468
- if is_gross:
469
- amount_basis = "gross"
470
- elif is_net:
471
- amount_basis = "net"
472
- elif clarify_vat_basis in (None, "לא Χ¦Χ•Χ™ΧŸ"):
473
- total = sum(i.get("unit_price",0)*i.get("quantity",1) for i in parse.get("items",[]))
474
- questions["vat_basis"] = f"הבכום (β‚ͺ{total:,.0f}) β€” Χ›Χ•ΧœΧœ מג״מ או ΧœΧ€Χ Χ™ מג״מ?"
475
- amount_basis = "net"
476
- else:
477
- amount_basis = "net"
478
- else:
479
- amount_basis = "net"
480
- else:
481
- # Auto: VAT cue β†’ tax_invoice_receipt; no cue β†’ receipt (no VAT)
482
- if has_vat_cue:
483
- doc_type, amount_basis = "tax_invoice_receipt", "gross" if is_gross else "net"
484
- else:
485
- doc_type, amount_basis = "receipt", "net"
486
-
487
- # ── 5. Allocation: Χ—.Χ€. only for Χ‘Χ’"מ/Χ—Χ‘Χ¨Χ” clients + large amounts ───────
488
- if doc_type in ("tax_invoice", "tax_invoice_receipt") and not exempt:
489
- subtotal = sum(it.get("unit_price",0)*it.get("quantity",1) for it in parse.get("items",[]))
490
- threshold = core.allocation_threshold_for_date(_dt.date.fromisoformat(parse["date"]))
491
- name = parse.get("client_name", "")
492
- is_corp = any(kw in name for kw in ['Χ‘Χ’"מ', "Χ‘Χ’Χž", "Χ—Χ‘Χ¨Χ”", "Χ‘Χ’'מ", "Χ‘Χ’.מ"])
493
- if is_corp and subtotal >= threshold:
494
- if clarify_client_tax_id and clarify_client_tax_id.strip():
495
- parse["client_tax_id"] = clarify_client_tax_id.strip()
496
- elif not parse.get("client_tax_id"):
497
- questions["client_tax_id"] = f"Χ—.Χ€. Χ”ΧœΧ§Χ•Χ— (Χ Χ“Χ¨Χ© ΧœΧ”Χ§Χ¦ΧΧ” β€” Χ’Χ‘Χ§Χ” β‰₯ β‚ͺ{threshold:,})"
498
-
499
- parse.update({"doc_type": doc_type, "amount_basis": amount_basis})
500
- parse.setdefault("payment_method", "bank_transfer")
501
- parse.setdefault("currency", "ILS")
502
- parse.setdefault("client_tax_id", None)
503
- parse.setdefault("client_is_business", False)
504
- parse.setdefault("client_name", "β€”")
505
- parse.setdefault("items", [])
506
-
507
- return issuer, parse, questions
508
-
509
-
510
- def _build_clarification_message(questions: dict, note_text: str) -> str:
511
- """Build ONE unified clarification message listing all missing fields."""
512
- lines = []
513
- for key, q in questions.items():
514
- if key == "issuer_status":
515
- lines.append(f"πŸ‘€ {q}")
516
- elif key == "client_name":
517
- lines.append(f"πŸ‘€ **שם ΧœΧ§Χ•Χ—:** {q}")
518
- elif key == "items":
519
- lines.append(f"πŸ“¦ **Χ€Χ¨Χ™Χ˜Χ™Χ:** {q}")
520
- elif key == "vat_basis":
521
- lines.append(f"πŸ’° **Χ‘Χ‘Χ™Χ‘ מג״מ:** {q}")
522
- elif key == "client_tax_id":
523
- lines.append(f"πŸ”– **הקצאה:** {q}")
524
- else:
525
- lines.append(f"β€’ {q}")
526
-
527
- fields_text = "\n".join(lines)
528
- return (f"❓ **Χ›Χ“Χ™ ΧœΧ”Χ€Χ™Χ§ אΧͺ Χ”ΧžΧ‘ΧžΧš, Χ—Χ‘Χ¨ ΧžΧ™Χ“Χ’:**\n\n"
529
- f"{fields_text}\n\n"
530
- f"Χ”Χ•Χ‘Χ£ אΧͺ Χ”Χ€Χ¨Χ˜Χ™Χ ל׀ΧͺΧ§ Χ•ΧœΧ—Χ₯ **Χ”Χ€Χ§ מבמך** Χ©Χ•Χ‘.")
531
-
532
-
533
- def generate(note, issuer_radio, doc_choice,
534
- clarify_vat, clarify_tax_id):
535
- """Single-shot, stateless pipeline."""
536
  note = (note or "").strip()
537
  if not note:
538
- return ("✏️ Χ›ΧͺΧ•Χ‘ Χ€ΧͺΧ§ Χ”Χ›Χ Χ‘Χ” Χ•ΧœΧ—Χ₯ Χ”Χ€Χ§ מבמך.",
539
- "", "", "", "", False)
540
 
541
  issuer_status = issuer_radio if issuer_radio in ("Χ€Χ˜Χ•Χ¨", "ΧžΧ•Χ¨Χ©Χ”") else None
542
 
543
- # ── Check issuer BEFORE running the model (saves 90s on CPU) ─────────────
544
  if not issuer_status:
545
- return ("πŸ‘€ Χ‘Χ—Χ¨ ΧͺΧ—Χ™ΧœΧ”: **Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨** או **Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ”** ΧœΧžΧ’ΧœΧ”.",
546
- "", "", "", "", False)
547
 
548
  _lazy_init()
549
  parse = model_parse(note)
550
 
551
  if parse is None or _missing_fields(parse) == ["parse_failed"]:
552
- return ("πŸ€” לא Χ”Χ¦ΧœΧ—ΧͺΧ™ ΧœΧ—ΧœΧ₯ Χ€Χ¨Χ˜Χ™Χ. Χ Χ‘Χ— ΧžΧ—Χ“Χ© β€” ΧœΧ“Χ•Χ’ΧžΧ”: "
553
- "'Χ§Χ™Χ‘ΧœΧͺΧ™ 500β‚ͺ ΧžΧžΧ©Χ” גל Χ™Χ™Χ’Χ•Χ₯'.",
554
- "", "", "", "", False)
555
 
556
- cvat = clarify_vat if clarify_vat not in (None, "לא Χ¦Χ•Χ™ΧŸ") else None
557
  ctax = clarify_tax_id.strip() if clarify_tax_id and clarify_tax_id.strip() else None
558
 
559
  issuer, final, questions = decide_fiscal(
560
  note, issuer_status, parse, doc_choice, cvat, ctax)
561
 
562
  if questions:
563
- msg = _build_clarification_message(questions, note)
564
- return (msg, "", "", "", "", True)
 
 
565
 
566
  try:
567
  completed = core.complete(issuer, final, _STATE["rng"])
568
  except Exception as e:
569
- return (f"⚠️ שגיאה: {e}", "", "", "", "", False)
570
 
571
  show_vat = (final["doc_type"] != "receipt")
572
 
573
  return ("βœ… Χ”ΧžΧ‘ΧžΧš Χ”Χ•Χ€Χ§ Χ‘Χ”Χ¦ΧœΧ—Χ”.",
574
- "",
575
  render_parse_panel(final, completed, show_vat),
576
  render_document(completed, show_vat),
577
  render_recommendations(recommend(note)),
578
- False)
 
579
 
580
- # ═════════════════════════════════════════════════════════════════════════════
581
- # 7. UI β€” Gradio 6.17 compatible, ChatGPT/Claude style
582
- # ═════════════════════════════════════════════════════════════════════════════
583
  CSS = """
584
  .gradio-container { max-width: 880px !important; margin: 0 auto !important; }
585
  footer { display: none !important; }
@@ -590,13 +564,12 @@ footer { display: none !important; }
590
  color: #0f172a !important;
591
  }
592
  .qs-btn:hover { background: #e2e8f0 !important; }
593
- .btn-send { font-weight: 600 !important; }
594
  """
595
 
596
  QUICK_STARTERS = [
597
- ("πŸ’Ό Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ” + ΧœΧ§Χ•Χ— Χ’Χ‘Χ§Χ™", 'Χ—Χ‘Χ¨Χͺ ΧžΧ’Χ’Χœ Χ‘Χ’"מ Χ©Χ™ΧœΧžΧ” 36,500 Χ©"Χ— גל Χ€Χ¨Χ•Χ™Χ§Χ˜ אΧͺΧ¨. Χ”Χ’Χ‘Χ¨Χ” בנקאיΧͺ.'),
598
  ("🧾 Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨ β€” Χ©Χ™Χ’Χ•Χ¨ Χ€Χ¨Χ˜Χ™", "Χ§Χ™Χ‘ΧœΧͺΧ™ 220 שקל ΧžΧ“Χ‘Χ™Χ¨ גל Χ©Χ™Χ’Χ•Χ¨ Χ©Χ—Χ™Χ™Χ” Χ€Χ¨Χ˜Χ™, ΧžΧ–Χ•ΧžΧŸ"),
599
- ("πŸ“¦ Χ›ΧžΧ” Χ€Χ¨Χ™Χ˜Χ™Χ", 'Χ Χ•Χ’Χ” Χ›Χ”ΧŸ Χ©Χ™ΧœΧžΧ” 2Γ—350 Χ©"Χ— Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™ + 180 Χ©"Χ— Χ”Χ›Χ Χͺ ΧžΧ¦Χ’Χͺ'),
600
  ]
601
 
602
  with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
@@ -611,17 +584,15 @@ with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
611
  </div>
612
  </div>""")
613
 
614
- # ── Who am I (required, no default) ─────────────────────────────────────
615
  issuer_radio = gr.Radio(
616
  ["Χ€Χ˜Χ•Χ¨", "ΧžΧ•Χ¨Χ©Χ”"], value=None,
617
  label="ΧžΧ™ אני (Χ‘Χ˜Χ˜Χ•Χ‘ ΧžΧ Χ€Χ™Χ§)",
618
- info="Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨ = ללא מג\"מ Β· Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ” = גם מג\"מ",
619
  )
620
 
621
  note_input = gr.Textbox(
622
- label="Χ€ΧͺΧ§ / ΧͺΧ•Χ›ΧŸ Χ”Χ”Χ›Χ Χ‘Χ”", lines=2,
623
  placeholder='ΧœΧ“Χ•Χ’ΧžΧ”: Χ§Χ™Χ‘ΧœΧͺΧ™ 1,200 Χ©"Χ— ΧžΧžΧ©Χ” Χ›Χ”ΧŸ גל Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™',
624
- rtl=True, autofocus=True,
625
  )
626
 
627
  doc_choice = gr.Radio(
@@ -636,18 +607,20 @@ with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
636
 
637
  status_output = gr.Markdown("")
638
 
639
- # ── Clarification fields β€” visible only when needed ──────────────────────
640
- with gr.Group(visible=False) as clarify_group:
641
  gr.HTML('<div dir="rtl" style="font-size:12px;font-weight:600;color:#7c3aed;'
642
- 'margin-bottom:8px;">πŸ“‹ Χ€Χ¨Χ˜Χ™Χ נוב׀ים נדרשים</div>')
643
  clarify_vat = gr.Radio(
644
  ["Χ›Χ•ΧœΧœ מג״מ", "ΧœΧ€Χ Χ™ מג״מ", "לא Χ¦Χ•Χ™ΧŸ"],
645
  value="לא Χ¦Χ•Χ™ΧŸ", label="הבכום Χ”ΧžΧ¦Χ•Χ™ΧŸ Χ‘Χ€ΧͺΧ§",
646
  )
 
 
 
 
647
  clarify_tax_id = gr.Textbox(
648
- label="Χ—.Χ€. / Χͺ.Χ–. ΧœΧ§Χ•Χ— (Χ Χ“Χ¨Χ© למב׀ר הקצאה)",
649
- placeholder="למשל: 514123458",
650
- value="",
651
  )
652
 
653
  gr.HTML('<div dir="rtl" style="font-size:11px;color:#94a3b8;margin:10px 0 6px;'
@@ -661,153 +634,34 @@ with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
661
  doc_output = gr.HTML()
662
  recs_output = gr.HTML()
663
 
664
- # ── Handlers ─────────────────────────────────────────────────────────────
665
  def _on_generate(note, issuer_r, doc_c, cvat, ctax):
666
- status, chtml, phtml, dhtml, rhtml, show_clarify = generate(
667
  note, issuer_r, doc_c, cvat, ctax)
668
  return (status, phtml, dhtml, rhtml,
669
- gr.update(visible=show_clarify))
 
670
 
671
  submit_btn.click(
672
  fn=_on_generate,
673
  inputs=[note_input, issuer_radio, doc_choice, clarify_vat, clarify_tax_id],
674
- outputs=[status_output, parse_output, doc_output, recs_output, clarify_group],
 
675
  )
676
 
677
  def _on_clear():
678
  return ("", None, "Χ–Χ™Χ”Χ•Χ™ ΧΧ•Χ˜Χ•ΧžΧ˜Χ™", "לא Χ¦Χ•Χ™ΧŸ", "", "", "", "", "",
679
- gr.update(visible=False))
680
 
681
  clear_btn.click(
682
  fn=_on_clear,
683
  outputs=[note_input, issuer_radio, doc_choice, clarify_vat, clarify_tax_id,
684
- status_output, parse_output, doc_output, recs_output, clarify_group],
685
- )
686
-
687
- # ═════════════════════════════════════════════════════════════════════════════
688
- # 8. Discord bot β€” runs as a background thread inside the Space (+5% bonus)
689
- # ═════════════════════════════════════════════════════════════════════════════
690
- # The bot shares the SAME model loaded by _lazy_init(), so no extra memory.
691
- # Token is read from the DISCORD_TOKEN secret (Space β†’ Settings β†’ Secrets).
692
- # If the secret is absent, the bot is silently skipped and the UI still works.
693
- import threading, asyncio
694
-
695
- def _format_doc_for_discord(completed: dict) -> str:
696
- """Render a completed fiscal document as Discord-friendly monospace text."""
697
- c = completed
698
- issuer = c.get("issuer", {})
699
- client = c.get("client", {})
700
- lines = c.get("lines", [])
701
- vat_pct = int(round(c.get("vat_rate", 0) * 100))
702
-
703
- line_rows = "\n".join(
704
- f" β€’ {ln['description']} Γ— {ln['quantity']} β†’ β‚ͺ{ln['line_total']:,.2f}"
705
- for ln in lines
706
  )
707
- alloc = (f"\nπŸ”– מב׀ר הקצאה: {c.get('allocation_number')}"
708
- if c.get("allocation_required") else "")
709
-
710
- return (
711
- f"```\n"
712
- f"{'━'*40}\n"
713
- f" {c.get('doc_type_he','מבמך')} | מב׳ {c.get('serial_number','')}\n"
714
- f" {c.get('issue_date','')}\n"
715
- f"{'━'*40}\n"
716
- f" ΧžΧ Χ€Χ™Χ§ : {issuer.get('name','')} (Χ—.Χ€. {issuer.get('tax_id','')})\n"
717
- f" ΧœΧ§Χ•Χ— : {client.get('name','')}"
718
- f"{' [Χ’Χ‘Χ§]' if client.get('is_business') else ''}\n"
719
- f"{'━'*40}\n"
720
- f"{line_rows}\n"
721
- f"{'━'*40}\n"
722
- f" בכום Χ Χ˜Χ• : β‚ͺ{c.get('subtotal',0):>12,.2f}\n"
723
- f" מג\"מ {vat_pct:2d}% : β‚ͺ{c.get('vat_amount',0):>12,.2f}\n"
724
- f" Χ‘Χ”\"Χ› : β‚ͺ{c.get('total',0):>12,.2f}\n"
725
- f" ΧͺΧ©ΧœΧ•Χ : {core.PAYMENT_HE.get(c.get('payment_method',''),'β€”')}\n"
726
- f"{'━'*40}\n"
727
- f"```"
728
- f"{alloc}"
729
- )
730
-
731
-
732
- def _discord_pipeline(raw_text: str) -> str:
733
- """Full pipeline for Discord: raw Hebrew note β†’ formatted document string.
734
- Uses safe defaults (no agentic loop in chat β€” Discord is single-shot)."""
735
- _lazy_init()
736
- parse = model_parse(raw_text)
737
- if parse is None:
738
- return "❌ לא Χ”Χ¦ΧœΧ—ΧͺΧ™ ΧœΧ—ΧœΧ₯ אΧͺ Χ”Χ€Χ¨Χ˜Χ™Χ. Χ Χ‘Χ” ΧœΧ Χ‘Χ— ΧžΧ—Χ“Χ©."
739
- final = apply_answers(parse, {}) # apply_answers fills safe defaults
740
- try:
741
- completed = core.complete(
742
- {**DEMO_ISSUER_BASE, "status": "authorized_dealer", "is_company": False},
743
- final, _STATE["rng"])
744
- except Exception as e:
745
- return f"❌ שגיאה Χ‘Χ’Χ™Χ‘Χ•Χ“: {e}"
746
- return _format_doc_for_discord(completed)
747
-
748
-
749
- def _start_discord_bot():
750
- """Launch the Discord bot in a background thread if DISCORD_TOKEN is set."""
751
- token = os.environ.get("DISCORD_TOKEN", "").strip()
752
- if not token:
753
- print("β„Ή DISCORD_TOKEN not set β€” Discord bot skipped (UI still runs).")
754
- return
755
-
756
- try:
757
- import discord
758
- from discord.ext import commands
759
- except ImportError:
760
- print("⚠ discord.py not installed β€” add 'discord.py' to requirements.txt")
761
- return
762
-
763
- intents = discord.Intents.default()
764
- intents.message_content = True
765
- bot = commands.Bot(command_prefix="!", intents=intents)
766
-
767
- @bot.event
768
- async def on_ready():
769
- print(f"βœ… Discord bot online: {bot.user}")
770
-
771
- @bot.command(name="receipt")
772
- async def receipt_cmd(ctx, *, note: str):
773
- await ctx.message.add_reaction("⏳")
774
- try:
775
- loop = asyncio.get_event_loop()
776
- result = await loop.run_in_executor(None, _discord_pipeline, note)
777
- await ctx.reply(result)
778
- except Exception as e:
779
- await ctx.reply(f"❌ שגיאה: {e}")
780
- finally:
781
- try:
782
- await ctx.message.remove_reaction("⏳", bot.user)
783
- await ctx.message.add_reaction("βœ…")
784
- except Exception:
785
- pass
786
-
787
- @bot.command(name="help_t2r")
788
- async def help_cmd(ctx):
789
- await ctx.reply(
790
- "**Text2Receipt Bot** 🧾\n"
791
- "Χ”ΧžΧ¨ Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ” Χ’Χ‘Χ¨Χ™Χͺ למבמך Χ€Χ™Χ‘Χ§ΧœΧ™ Χ™Χ©Χ¨ΧΧœΧ™.\n\n"
792
- "Χ©Χ™ΧžΧ•Χ©: `!receipt <Χ”Χ’Χ¨Χ” Χ‘Χ’Χ‘Χ¨Χ™Χͺ>`\n"
793
- "Χ“Χ•Χ’ΧžΧ”: `!receipt Χ§Χ™Χ‘ΧœΧͺΧ™ 500 שקל ΧžΧžΧ©Χ” גל Χ™Χ™Χ’Χ•Χ₯`"
794
- )
795
-
796
- def _run():
797
- try:
798
- asyncio.run(bot.start(token))
799
- except Exception as e:
800
- print(f"β„Ή Discord bot could not connect "
801
- f"(expected on HF Spaces β€” outbound to discord.com is blocked): "
802
- f"{type(e).__name__}. UI is unaffected.", flush=True)
803
-
804
- threading.Thread(target=_run, daemon=True).start()
805
- print("πŸ€– Discord bot thread started.")
806
-
807
- # ═════════════════════════════════════════════════════════════════════════════
808
- # 9. Launch
809
- # ═════════════════════════════════════════════════════════════════════════════
810
- _start_discord_bot()
811
 
 
 
 
812
  if __name__ == "__main__":
813
  demo.launch(css=CSS)
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ Text2Receipt β€” HF Space (final clean version)
4
+ ================================================
5
  Pipeline:
6
+ free-text Hebrew income note
7
+ β†’ gemma-2-2b fine-tuned parser (extracts client / items)
8
+ β†’ fiscal decision layer (VAT cues, doc-type, allocation)
9
+ β†’ t2r_core.complete() (deterministic arithmetic)
10
+ β†’ Gradio UI (render document + FAISS recommendations)
 
 
 
 
 
11
  """
12
+
13
+ import json, random, re as _re
 
 
 
14
  import numpy as np
15
  import gradio as gr
16
+ import t2r_core as core
17
+ import datetime as _dt
18
 
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ # 1. Lazy model + FAISS state
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+ BASE_MODEL = "unsloth/gemma-2-2b-it"
23
+ MODEL_REPO = "yonilev/Text2Receipt-parser"
24
 
25
+ _STATE = dict(ready=False, tok=None, model=None, enc=None,
26
+ index=None, store=None, e5_family=False,
27
+ rng=random.Random(42), device="cpu")
28
 
 
 
 
 
29
 
30
  def _lazy_init():
31
  if _STATE["ready"]:
 
92
  print(f"βœ… FAISS ready β€” {emb.shape[0]} vectors")
93
 
94
 
95
+ # ─────────────────────────────────────────────────────────────────────────────
96
+ # 2. Parser + post-processing cleaner
97
+ # ─────────────────────────────────────────────────────────────────────────────
98
  INSTRUCTION = (
99
  "אΧͺΧ” ΧžΧžΧ™Χ¨ Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ” Χ—Χ•Χ€Χ©Χ™Χͺ Χ‘Χ’Χ‘Χ¨Χ™Χͺ ΧœΧžΧ‘Χ Χ” JSON. "
100
  "Χ—ΧœΧ₯ אך Χ•Χ¨Χ§ אΧͺ ΧžΧ” Χ©Χ›ΧͺΧ•Χ‘ Χ‘Χ”Χ’Χ¨Χ”: שם Χ”ΧœΧ§Χ•Χ— (client_name), "
 
103
  "Χ”Χ—Χ–Χ¨ JSON ΧͺΧ§Χ™ΧŸ Χ‘ΧœΧ‘Χ“, ללא טקבט Χ Χ•Χ‘Χ£."
104
  )
105
 
106
+ _VAT_NOISE = [
107
+ "ΧœΧ€Χ Χ™ מג\"מ", "Χ›Χ•ΧœΧœ מג\"מ", "ΧœΧ€Χ Χ™ מג״מ", "Χ›Χ•ΧœΧœ מג״מ",
108
+ "Χ‘ΧͺΧ•Χ‘Χ€Χͺ מג\"מ", "+ מג\"מ", "+מג\"מ", "מג\"מ", "מג״מ", "מגמ",
109
+ ]
110
+ _NOISE_SET = {"ΧœΧ€Χ Χ™", "Χ›Χ•ΧœΧœ", "Χ Χ˜Χ•", "Χ‘Χ¨Χ•Χ˜Χ•", "מב", "+", "Χ€ΧœΧ•Χ‘",
111
+ "מג\"מ", "מג״מ", "מגמ"}
112
+
113
+
114
  def _build_prompt(raw_text):
115
  return f"{INSTRUCTION}\n\nΧ”Χ’Χ¨Χ”: {raw_text}\n\nJSON:"
116
 
117
+
118
  def _extract_json(text):
119
  s = text.find("{")
120
  if s < 0: return None
 
128
  except: return None
129
  return None
130
 
131
+
132
+ def _clean_name(name):
133
+ """Strip VAT-related noise the model may accidentally grab as a client name."""
134
+ name = (name or "").strip()
135
+ for tok in _VAT_NOISE:
136
+ name = name.replace(tok, "").strip(" ,.-+")
137
+ return "" if (name in _NOISE_SET or len(name) <= 1) else name
138
+
139
+
140
+ def _clean_parse(parse):
141
+ """Post-process raw model output: remove noise, validate structure."""
142
+ if not isinstance(parse, dict):
143
+ return parse
144
+ # Clean client name
145
+ parse["client_name"] = _clean_name(parse.get("client_name", ""))
146
+ # Clean items
147
+ clean_items = []
148
+ for it in (parse.get("items") or []):
149
+ desc = (it.get("description") or "").strip()
150
+ for tok in _VAT_NOISE:
151
+ desc = desc.replace(tok, "").strip(" ,.-+")
152
+ if desc in _NOISE_SET or len(desc) <= 0:
153
+ desc = "Χ©Χ™Χ¨Χ•Χͺ"
154
+ it = dict(it)
155
+ it["description"] = desc
156
+ if it.get("unit_price"):
157
+ clean_items.append(it)
158
+ parse["items"] = clean_items
159
+ return parse
160
+
161
+
162
  def model_parse(raw_text):
163
  tok, model = _STATE["tok"], _STATE["model"]
164
  msgs = [{"role": "user", "content": _build_prompt(raw_text)}]
 
169
  out = model.generate(**enc, max_new_tokens=128, do_sample=False,
170
  pad_token_id=tok.pad_token_id)
171
  decoded = tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
172
+ raw = _extract_json(decoded)
173
+ return _clean_parse(raw) if raw else None
174
 
 
 
 
 
 
 
175
 
176
  def _missing_fields(parse):
177
  if parse is None: return ["parse_failed"]
 
180
  if not parse.get("items"): m.append("items")
181
  return m
182
 
183
+
184
+ # ─────────────────────────────────────────────────────────────────────────────
185
+ # 3. FAISS recommender
186
+ # ────────────────────────────���────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  def recommend(query_text, k=3):
188
  pref = "query: " if _STATE["e5_family"] else ""
189
  q_emb = _STATE["enc"].encode([pref + query_text],
 
198
  return out
199
 
200
 
201
+ # ─────────────────────────────────────────────────────────────────────────────
202
+ # 4. Fiscal decision layer (app-level, never touches frozen t2r_core.py)
203
+ # ─────────────────────────────────────────────────────────────────────────────
204
+ DEMO_ISSUER_BASE = {"name": "ΧžΧ™Χ Χ©Χ§Χ˜Χ™Χ", "tax_id": "962569844"}
205
+
206
+ _GROSS_PATTERNS = [r"Χ›Χ•ΧœΧœ\s*מג[\"Χ΄]?מ", r"Χ›Χ•ΧœΧœ\s*מב", r"Χ’Χ¨Χ•Χ‘", r"Χ‘Χ¨Χ•Χ˜Χ•", r"ΧžΧ—Χ™Χ¨\s*Χ›Χ•ΧœΧœ"]
207
+ _NET_PATTERNS = [r"ΧœΧ€Χ Χ™\s*מג[\"Χ΄]?מ", r"\+\s*מג[\"Χ΄]?מ", r"Χ‘ΧͺΧ•Χ‘Χ€Χͺ\s*מג[\"Χ΄]?מ",
208
+ r"Χ€ΧœΧ•Χ‘\s*מג[\"Χ΄]?מ", r"Χ Χ˜Χ•", r"לא\s*Χ›Χ•ΧœΧœ\s*מג[\"Χ΄]?מ",
209
+ r"Χ‘ΧœΧ™\s*מג[\"Χ΄]?מ", r"ΧœΧ€Χ Χ™\s*מב"]
210
+
211
+ def _text_has(text, patterns):
212
+ t = text.lower()
213
+ return any(_re.search(p, t) for p in patterns)
214
+
215
+ def _detect_date(text):
216
+ for pat, order in [(r"(\d{1,2})[/\-\.](\d{1,2})[/\-\.](\d{2,4})", "dmy"),
217
+ (r"(\d{4})[/\-\.](\d{1,2})[/\-\.](\d{1,2})", "ymd")]:
218
+ m = _re.search(pat, text)
219
+ if m:
220
+ try:
221
+ g = [int(x) for x in m.groups()]
222
+ d, mo, y = (g[0], g[1], g[2]) if order == "dmy" else (g[2], g[1], g[0])
223
+ if y < 100: y += 2000
224
+ return f"{y:04d}-{mo:02d}-{d:02d}"
225
+ except Exception:
226
+ pass
227
+ return None
228
+
229
+ def _today_str():
230
+ return _dt.date.today().isoformat()
231
+
232
+
233
+ def decide_fiscal(note, issuer_status, parse, doc_radio,
234
+ clarify_vat=None, clarify_tax_id=None):
235
+ """
236
+ Returns (issuer_dict, final_parse, questions_dict).
237
+ questions_dict empty β†’ ready to generate.
238
+ """
239
+ # ── 1. Issuer required β€” early return if missing ──────────────────────
240
+ if not issuer_status:
241
+ return None, None, {"issuer_status": "Χ‘Χ—Χ¨ Χ‘Χ˜Χ˜Χ•Χ‘ ΧžΧ Χ€Χ™Χ§ ΧœΧžΧ’ΧœΧ” (Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨ / Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ”)."}
242
+
243
+ questions = {}
244
+ exempt = (issuer_status == "Χ€Χ˜Χ•Χ¨")
245
+ issuer = {**DEMO_ISSUER_BASE,
246
+ "status": "exempt_dealer" if exempt else "authorized_dealer",
247
+ "is_company": False}
248
+
249
+ # ── 2. Mandatory content ──────────────────────────────────────────────
250
+ if not parse.get("client_name", "").strip():
251
+ questions["client_name"] = "שם Χ”ΧœΧ§Χ•Χ— / Χ”ΧžΧ©ΧœΧ?"
252
+ if not parse.get("items"):
253
+ questions["items"] = "ΧžΧ” Χ”Χ©Χ™Χ¨Χ•Χͺ/Χ”ΧžΧ•Χ¦Χ¨ ובאיזה בכום? (למשל: Χ™Χ™Χ’Χ•Χ₯ β‚ͺ500)"
254
+
255
+ # ── 3. Date: detect from note β†’ today ────────────────────────────────
256
+ parse = dict(parse)
257
+ parse["date"] = _detect_date(note) or _today_str()
258
+
259
+ # ── 4. Doc type + VAT basis ───────────────────────────────────────────
260
+ is_gross = _text_has(note, _GROSS_PATTERNS) or clarify_vat == "Χ›Χ•ΧœΧœ מג״מ"
261
+ is_net = _text_has(note, _NET_PATTERNS) or clarify_vat == "ΧœΧ€Χ Χ™ מג״מ"
262
+ has_vat_cue = is_gross or is_net
263
+
264
+ if exempt:
265
+ doc_type, amount_basis = "receipt", "net"
266
+
267
+ elif doc_radio and doc_radio != "Χ–Χ™Χ”Χ•Χ™ ΧΧ•Χ˜Χ•ΧžΧ˜Χ™":
268
+ doc_type = {"Χ§Χ‘ΧœΧ”": "receipt",
269
+ "Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב": "tax_invoice",
270
+ "Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב Χ•Χ§Χ‘ΧœΧ”": "tax_invoice_receipt"}.get(doc_radio, "receipt")
271
+ if doc_type != "receipt":
272
+ if is_gross:
273
+ amount_basis = "gross"
274
+ elif is_net:
275
+ amount_basis = "net"
276
+ elif clarify_vat in (None, "לא Χ¦Χ•Χ™ΧŸ"):
277
+ total = sum(i.get("unit_price", 0) * i.get("quantity", 1)
278
+ for i in parse.get("items", []))
279
+ questions["vat_basis"] = f"הבכום (β‚ͺ{total:,.0f}) β€” Χ›Χ•ΧœΧœ מג״מ או ΧœΧ€Χ Χ™ מג״מ?"
280
+ amount_basis = "net"
281
+ else:
282
+ amount_basis = "net"
283
+ else:
284
+ amount_basis = "net"
285
+
286
+ else:
287
+ # Auto: VAT cue in note β†’ tax_invoice_receipt; otherwise β†’ receipt (no VAT line)
288
+ if has_vat_cue:
289
+ doc_type, amount_basis = "tax_invoice_receipt", "gross" if is_gross else "net"
290
+ else:
291
+ doc_type, amount_basis = "receipt", "net"
292
+
293
+ # ── 5. Allocation: only Χ‘Χ’"מ / Χ—Χ‘Χ¨Χ” + amount β‰₯ threshold ─────────────
294
+ if doc_type in ("tax_invoice", "tax_invoice_receipt") and not exempt:
295
+ subtotal = sum(it.get("unit_price", 0) * it.get("quantity", 1)
296
+ for it in parse.get("items", []))
297
+ threshold = core.allocation_threshold_for_date(
298
+ _dt.date.fromisoformat(parse["date"]))
299
+ name = parse.get("client_name", "")
300
+ is_corp = any(kw in name for kw in ['Χ‘Χ’"מ', "Χ‘Χ’Χž", "Χ—Χ‘Χ¨Χ”", "Χ‘Χ’'מ", "Χ‘Χ’.מ"])
301
+ if is_corp and subtotal >= threshold:
302
+ if clarify_tax_id and clarify_tax_id.strip():
303
+ parse["client_tax_id"] = clarify_tax_id.strip()
304
+ elif not parse.get("client_tax_id"):
305
+ questions["client_tax_id"] = f"Χ—.Χ€. Χ”ΧœΧ§Χ•Χ— (Χ Χ“Χ¨Χ© ΧœΧ”Χ§Χ¦ΧΧ” β€” Χ’Χ‘Χ§Χ” β‰₯ β‚ͺ{threshold:,})"
306
+
307
+ # ── 6. Finalise parse ─────────────────────────────────────────────────
308
+ parse.update({"doc_type": doc_type, "amount_basis": amount_basis})
309
+ parse.setdefault("payment_method", "bank_transfer")
310
+ parse.setdefault("currency", "ILS")
311
+ parse.setdefault("client_tax_id", None)
312
+ parse.setdefault("client_is_business", False)
313
+ parse.setdefault("client_name", "β€”")
314
+ parse.setdefault("items", [])
315
+
316
+ return issuer, parse, questions
317
+
318
+
319
+ def _build_clarification_message(questions):
320
+ icons = {"issuer_status": "πŸ‘€", "client_name": "πŸ‘€",
321
+ "items": "πŸ“¦", "vat_basis": "πŸ’°", "client_tax_id": "πŸ”–"}
322
+ lines = "\n".join(f"{icons.get(k,'β€’')} {v}" for k, v in questions.items())
323
+ return f"❓ **Χ›Χ“Χ™ ΧœΧ”Χ€Χ™Χ§ אΧͺ Χ”ΧžΧ‘ΧžΧš, Χ—Χ‘Χ¨ ΧžΧ™Χ“Χ’:**\n\n{lines}\n\nΧ”Χ•Χ‘Χ£ אΧͺ Χ”Χ€Χ¨Χ˜Χ™Χ ל׀ΧͺΧ§ Χ•ΧœΧ—Χ₯ **Χ”Χ€Χ§ מבמך** Χ©Χ•Χ‘."
324
+
325
+
326
+ # ─────────────────────────────────────────────────────────────────────────────
327
  # 5. HTML renderers
328
+ # ─────────────────────────────────────────────────────────────────────────────
329
+ def render_parse_panel(parse, completed, show_vat):
330
+ items = parse.get("items", [])
331
+ item_rows = "".join(
332
+ f'<div style="display:flex;justify-content:space-between;font-size:12px;'
333
+ f'color:#334155;padding:3px 0;"><span>{it.get("description","")}</span>'
334
+ f'<span style="color:#64748b;">{it.get("quantity",1)} Γ— β‚ͺ{it.get("unit_price",0):,.0f}</span></div>'
335
+ for it in items)
336
+
337
+ chips = (
338
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
339
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">ΧœΧ§Χ•Χ—: {parse.get("client_name","β€”")}</span> '
340
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
341
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">'
342
+ f'{"Χ’Χ‘Χ§" if parse.get("client_is_business") else "Χ€Χ¨Χ˜Χ™"}</span> '
343
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
344
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">{len(items)} Χ€Χ¨Χ™Χ˜Χ™Χ</span>')
345
+
346
+ alloc_line = (
347
+ '<div style="font-size:12px;color:#334155;padding:3px 0;">'
348
+ 'מב׀ר הקצאה <span style="color:#0d9488;">βœ“ Χ Χ“Χ¨Χ©</span></div>'
349
+ if completed.get("allocation_required") else "")
350
+
351
+ vat_line = (
352
+ f'<div style="font-size:12px;color:#334155;padding:3px 0;">'
353
+ f'מג"מ {int(round(completed.get("vat_rate",0)*100))}% Β· β‚ͺ{completed.get("vat_amount",0):,.2f}</div>'
354
+ if show_vat else
355
+ '<div style="font-size:12px;color:#94a3b8;padding:3px 0;">ללא Χ€Χ™Χ¨Χ•Χ§ מג"מ (Χ§Χ‘ΧœΧ”)</div>')
356
+
357
+ return f"""
358
+ <div dir="rtl" style="font-family:'Segoe UI',Arial,sans-serif;max-width:660px;
359
+ margin:14px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
360
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
361
+ <div style="font-size:11px;font-weight:700;color:#0d9488;letter-spacing:.05em;
362
+ text-transform:uppercase;margin-bottom:10px;">🧠 Χ”ΧžΧ•Χ“Χœ Χ—Χ™ΧœΧ₯</div>
363
+ <div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px;">{chips}</div>
364
+ {item_rows}
365
+ <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
366
+ padding-top:8px;">gemma-2-2b + LoRA Β· Χ—Χ•Χ–Χ” Χ¨Χ§ <code>parse</code></div>
367
+ </div>
368
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
369
+ <div style="font-size:11px;font-weight:700;color:#7c3aed;letter-spacing:.05em;
370
+ text-transform:uppercase;margin-bottom:10px;">βš™οΈ Χ”ΧžΧ Χ•Χ’ Χ—Χ™Χ©Χ‘</div>
371
+ {vat_line}
372
+ <div style="font-size:12px;color:#334155;padding:3px 0;">Χ‘Χ”"Χ› β‚ͺ{completed.get("total",0):,.2f}</div>
373
+ <div style="font-size:12px;color:#334155;padding:3px 0;">מב׳ מבמך {completed.get("serial_number","")}</div>
374
+ {alloc_line}
375
+ <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
376
+ padding-top:8px;">Χ“Χ˜Χ¨ΧžΧ™Χ Χ™Χ‘Χ˜Χ™ Β· א׀ב Χ”Χ–Χ™Χ•Χͺ Χ‘Χ—Χ©Χ‘Χ•ΧŸ</div>
377
+ </div>
378
+ </div>"""
379
+
380
+
381
  def render_document(c, show_vat=True):
382
  issuer = c.get("issuer", {})
383
  client = c.get("client", {})
 
398
  f'</tr>'
399
  for ln in lines)
400
 
401
+ vat_block = "" if not show_vat else f"""
402
+ <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:8px;">
403
+ <span>בכום ΧœΧ€Χ Χ™ מג&quot;מ</span>
404
+ <span style="color:#94a3b8;">β‚ͺ{c.get("subtotal",0):,.2f}</span>
405
+ </div>
406
+ <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;
407
+ padding-bottom:12px;border-bottom:1px solid #334155;margin-bottom:12px;">
408
+ <span>מג&quot;מ {vat_pct}%</span>
409
+ <span style="color:#94a3b8;">β‚ͺ{c.get("vat_amount",0):,.2f}</span>
410
+ </div>"""
411
+
412
  return f"""
413
  <div dir="rtl" style="font-family:'Segoe UI','Arial Hebrew',Arial,sans-serif;
414
  background:#0f172a;color:#e2e8f0;border-radius:16px;
 
465
  </table>
466
 
467
  <div style="background:#1e293b;border-radius:10px;padding:16px 18px;">
468
+ {vat_block}
 
 
 
 
 
 
 
 
 
469
  <div style="display:flex;justify-content:space-between;align-items:baseline;">
470
  <span style="font-size:15px;font-weight:600;color:#e2e8f0;">Χ‘Χ”"Χ› לΧͺΧ©ΧœΧ•Χ</span>
471
  <span style="font-size:24px;font-weight:700;color:#2dd4bf;">β‚ͺ{c.get("total",0):,.2f}</span>
 
500
  </div>"""
501
 
502
 
503
+ # ─────────────────────────────────────────────────────────────────────────────
504
+ # 6. Main pipeline
505
+ # ─────────────────────────────────────────────────────────────────────────────
506
+ def generate(note, issuer_radio, doc_choice, clarify_vat, clarify_tax_id):
507
+ """
508
+ Stateless single handler.
509
+ Returns: (status_md, parse_html, doc_html, recs_html, vat_group_visible, tax_group_visible)
510
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  note = (note or "").strip()
512
  if not note:
513
+ return "✏️ Χ›ΧͺΧ•Χ‘ Χ€ΧͺΧ§ Χ”Χ›Χ Χ‘Χ” Χ•ΧœΧ—Χ₯ Χ”Χ€Χ§ מבמך.", "", "", "", False, False
 
514
 
515
  issuer_status = issuer_radio if issuer_radio in ("Χ€Χ˜Χ•Χ¨", "ΧžΧ•Χ¨Χ©Χ”") else None
516
 
517
+ # Issuer check BEFORE model (saves ~90s on CPU)
518
  if not issuer_status:
519
+ return "πŸ‘€ Χ‘Χ—Χ¨ ΧͺΧ—Χ™ΧœΧ”: **Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨** או **Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ”** ΧœΧžΧ’ΧœΧ”.", "", "", "", False, False
 
520
 
521
  _lazy_init()
522
  parse = model_parse(note)
523
 
524
  if parse is None or _missing_fields(parse) == ["parse_failed"]:
525
+ return ("πŸ€” לא Χ”Χ¦ΧœΧ—ΧͺΧ™ ΧœΧ—ΧœΧ₯ Χ€Χ¨Χ˜Χ™Χ. Χ Χ‘Χ— ΧžΧ—Χ“Χ© β€” ΧœΧ“Χ•Χ’ΧžΧ”: 'Χ§Χ™Χ‘ΧœΧͺΧ™ 500β‚ͺ ΧžΧžΧ©Χ” גל Χ™Χ™Χ’Χ•Χ₯'.",
526
+ "", "", "", False, False)
 
527
 
528
+ cvat = clarify_vat if clarify_vat not in (None, "לא Χ¦Χ•Χ™ΧŸ") else None
529
  ctax = clarify_tax_id.strip() if clarify_tax_id and clarify_tax_id.strip() else None
530
 
531
  issuer, final, questions = decide_fiscal(
532
  note, issuer_status, parse, doc_choice, cvat, ctax)
533
 
534
  if questions:
535
+ show_vat_q = "vat_basis" in questions
536
+ show_taxid_q = "client_tax_id" in questions
537
+ return (_build_clarification_message(questions),
538
+ "", "", "", show_vat_q, show_taxid_q)
539
 
540
  try:
541
  completed = core.complete(issuer, final, _STATE["rng"])
542
  except Exception as e:
543
+ return f"⚠️ שגיאה Χ‘Χ’Χ™Χ‘Χ•Χ“: {e}", "", "", "", False, False
544
 
545
  show_vat = (final["doc_type"] != "receipt")
546
 
547
  return ("βœ… Χ”ΧžΧ‘ΧžΧš Χ”Χ•Χ€Χ§ Χ‘Χ”Χ¦ΧœΧ—Χ”.",
 
548
  render_parse_panel(final, completed, show_vat),
549
  render_document(completed, show_vat),
550
  render_recommendations(recommend(note)),
551
+ False, False)
552
+
553
 
554
+ # ─────────────────────────────────────────────────────────────────────────────
555
+ # 7. Gradio UI
556
+ # ─────────────────────────────────────────────────────────────────────────────
557
  CSS = """
558
  .gradio-container { max-width: 880px !important; margin: 0 auto !important; }
559
  footer { display: none !important; }
 
564
  color: #0f172a !important;
565
  }
566
  .qs-btn:hover { background: #e2e8f0 !important; }
 
567
  """
568
 
569
  QUICK_STARTERS = [
570
+ ("πŸ’Ό Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ” + ΧœΧ§Χ•Χ— Χ’Χ‘Χ§Χ™", 'Χ—Χ‘Χ¨Χͺ ΧžΧ’Χ’Χœ Χ‘Χ’"מ Χ©Χ™ΧœΧžΧ” 36,500 Χ›Χ•ΧœΧœ מג"מ גל Χ€Χ¨Χ•Χ™Χ§Χ˜ אΧͺΧ¨. Χ”Χ’Χ‘Χ¨Χ” בנקאיΧͺ.'),
571
  ("🧾 Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨ β€” Χ©Χ™Χ’Χ•Χ¨ Χ€Χ¨Χ˜Χ™", "Χ§Χ™Χ‘ΧœΧͺΧ™ 220 שקל ΧžΧ“Χ‘Χ™Χ¨ גל Χ©Χ™Χ’Χ•Χ¨ Χ©Χ—Χ™Χ™Χ” Χ€Χ¨Χ˜Χ™, ΧžΧ–Χ•ΧžΧŸ"),
572
+ ("πŸ“¦ Χ›ΧžΧ” Χ€Χ¨Χ™Χ˜Χ™Χ", 'Χ Χ•Χ’Χ” Χ›Χ”ΧŸ Χ©Χ™ΧœΧžΧ” 2Γ—350 Χ©"Χ— Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™ + 180 Χ©"Χ— Χ”Χ›Χ Χͺ ΧžΧ¦Χ’Χͺ, ΧœΧ€Χ Χ™ מג"מ'),
573
  ]
574
 
575
  with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
 
584
  </div>
585
  </div>""")
586
 
 
587
  issuer_radio = gr.Radio(
588
  ["Χ€Χ˜Χ•Χ¨", "ΧžΧ•Χ¨Χ©Χ”"], value=None,
589
  label="ΧžΧ™ אני (Χ‘Χ˜Χ˜Χ•Χ‘ ΧžΧ Χ€Χ™Χ§)",
590
+ info='Χ’Χ•Χ‘Χ§ Χ€Χ˜Χ•Χ¨ = ללא מג"מ Β· Χ’Χ•Χ‘Χ§ ΧžΧ•Χ¨Χ©Χ” = גם מג"מ',
591
  )
592
 
593
  note_input = gr.Textbox(
594
+ label="Χ€ΧͺΧ§ / ΧͺΧ•Χ›ΧŸ Χ”Χ”Χ›Χ Χ‘Χ”", lines=2, rtl=True, autofocus=True,
595
  placeholder='ΧœΧ“Χ•Χ’ΧžΧ”: Χ§Χ™Χ‘ΧœΧͺΧ™ 1,200 Χ©"Χ— ΧžΧžΧ©Χ” Χ›Χ”ΧŸ גל Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™',
 
596
  )
597
 
598
  doc_choice = gr.Radio(
 
607
 
608
  status_output = gr.Markdown("")
609
 
610
+ # Clarification fields β€” appear only when needed
611
+ with gr.Group(visible=False) as clarify_vat_group:
612
  gr.HTML('<div dir="rtl" style="font-size:12px;font-weight:600;color:#7c3aed;'
613
+ 'margin-bottom:6px;">πŸ’° Χ‘Χ‘Χ™Χ‘ מג״מ Χ Χ“Χ¨Χ©</div>')
614
  clarify_vat = gr.Radio(
615
  ["Χ›Χ•ΧœΧœ מג״מ", "ΧœΧ€Χ Χ™ מג״מ", "לא Χ¦Χ•Χ™ΧŸ"],
616
  value="לא Χ¦Χ•Χ™ΧŸ", label="הבכום Χ”ΧžΧ¦Χ•Χ™ΧŸ Χ‘Χ€ΧͺΧ§",
617
  )
618
+
619
+ with gr.Group(visible=False) as clarify_tax_group:
620
+ gr.HTML('<div dir="rtl" style="font-size:12px;font-weight:600;color:#7c3aed;'
621
+ 'margin-bottom:6px;">πŸ”– Χ—.Χ€. ΧœΧ§Χ•Χ— Χ Χ“Χ¨Χ© (הקצאה)</div>')
622
  clarify_tax_id = gr.Textbox(
623
+ label="Χ—.Χ€. / Χͺ.Χ–. ΧœΧ§Χ•Χ—", placeholder="למשל: 514123458", value="",
 
 
624
  )
625
 
626
  gr.HTML('<div dir="rtl" style="font-size:11px;color:#94a3b8;margin:10px 0 6px;'
 
634
  doc_output = gr.HTML()
635
  recs_output = gr.HTML()
636
 
637
+ # Handlers
638
  def _on_generate(note, issuer_r, doc_c, cvat, ctax):
639
+ status, phtml, dhtml, rhtml, show_vat_q, show_taxid_q = generate(
640
  note, issuer_r, doc_c, cvat, ctax)
641
  return (status, phtml, dhtml, rhtml,
642
+ gr.update(visible=show_vat_q),
643
+ gr.update(visible=show_taxid_q))
644
 
645
  submit_btn.click(
646
  fn=_on_generate,
647
  inputs=[note_input, issuer_radio, doc_choice, clarify_vat, clarify_tax_id],
648
+ outputs=[status_output, parse_output, doc_output, recs_output,
649
+ clarify_vat_group, clarify_tax_group],
650
  )
651
 
652
  def _on_clear():
653
  return ("", None, "Χ–Χ™Χ”Χ•Χ™ ΧΧ•Χ˜Χ•ΧžΧ˜Χ™", "לא Χ¦Χ•Χ™ΧŸ", "", "", "", "", "",
654
+ gr.update(visible=False), gr.update(visible=False))
655
 
656
  clear_btn.click(
657
  fn=_on_clear,
658
  outputs=[note_input, issuer_radio, doc_choice, clarify_vat, clarify_tax_id,
659
+ status_output, parse_output, doc_output, recs_output,
660
+ clarify_vat_group, clarify_tax_group],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
 
663
+ # ─────────────────────────────────────────────────────────────────────────────
664
+ # 8. Launch
665
+ # ─────────────────────────────────────────────────────────────────────────────
666
  if __name__ == "__main__":
667
  demo.launch(css=CSS)