yonilev commited on
Commit
6456aa6
ยท
verified ยท
1 Parent(s): 71fc6e2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +548 -148
app.py CHANGED
@@ -1,180 +1,580 @@
1
- import gradio as gr
2
- import pandas as pd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import numpy as np
4
- import torch
5
- from datasets import load_dataset
6
- from sentence_transformers import SentenceTransformer
7
- from sklearn.metrics.pairwise import cosine_similarity
8
-
9
- # โ”€โ”€ Startup: load everything once โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
- print("Loading CLIP model...")
11
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
12
- model = SentenceTransformer("clip-ViT-B-32", device=DEVICE)
13
-
14
- print("Loading embeddings...")
15
- embed_df = pd.read_parquet("fashion_embeddings.parquet")
16
- EMBED_MATRIX = np.array(embed_df["embedding"].tolist(), dtype=np.float32)
17
-
18
- print("Loading dataset (for images)...")
19
- dataset = load_dataset("ashraq/fashion-product-images-small", split="train")
20
- id_to_idx = {int(dataset[i]["id"]): i for i in range(len(dataset))}
21
-
22
- CATEGORIES = ["All"] + sorted(embed_df["masterCategory"].unique().tolist())
23
- GENDERS = ["All"] + sorted(embed_df["gender"].unique().tolist())
24
- print(f"Ready โ€” {len(embed_df):,} items indexed on {DEVICE}")
25
-
26
-
27
- # โ”€โ”€ Core logic โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
28
- def _mmr_select(similarities, filtered_sims, n, lam=0.7):
29
- selected, candidates = [], np.where(filtered_sims > -1)[0].tolist()
30
- while len(selected) < n and candidates:
31
- if not selected:
32
- best = candidates[int(np.argmax(filtered_sims[candidates]))]
33
- else:
34
- scores = [
35
- lam * similarities[c]
36
- - (1 - lam) * cosine_similarity(EMBED_MATRIX[c:c+1], EMBED_MATRIX[selected]).max()
37
- for c in candidates
38
- ]
39
- best = candidates[int(np.argmax(scores))]
40
- selected.append(best); candidates.remove(best)
41
- return selected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
 
43
 
44
- def get_recommendations(query, category="All", gender="All", diversity=False, n=3):
45
- emb = model.encode(query, convert_to_numpy=True).reshape(1, -1)
46
- sims = cosine_similarity(emb, EMBED_MATRIX)[0]
47
 
48
- fsims = sims.copy()
49
- if category != "All":
50
- fsims[(embed_df["masterCategory"] != category).values] = -1
51
- if gender != "All":
52
- fsims[(embed_df["gender"] != gender).values] = -1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- indices = _mmr_select(sims, fsims, n) if diversity \
55
- else np.argsort(fsims)[::-1][:n].tolist()
56
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  results = []
58
- for idx in indices:
59
- row = embed_df.iloc[idx]
60
- ds_idx = id_to_idx.get(int(row["id"]), -1)
61
  results.append({
62
- "image": dataset[ds_idx]["image"] if ds_idx >= 0 else None,
63
- "name": row["productDisplayName"],
64
- "article": row["articleType"],
65
- "color": row["baseColour"],
66
- "gender": row["gender"],
67
- "similarity": float(sims[idx]),
68
  })
69
  return results
70
 
71
 
72
- def build_info_html(results):
73
- html = '<div style="display:flex;gap:8px;margin-top:8px;">'
74
- for i, r in enumerate(results, 1):
75
- name = r["name"][:40] + "โ€ฆ" if len(r["name"]) > 40 else r["name"]
76
- html += f"""
77
- <div style="flex:1;padding:10px;border:1px solid #e0e0e0;border-radius:10px;text-align:center;background:#fafafa;">
78
- <b style="font-size:15px;">#{i}</b>
79
- <span style="color:#f59e0b;font-size:15px;"> โญ {r['similarity']:.3f}</span><br>
80
- <span style="font-size:12px;">{name}</span><br>
81
- <span style="color:#666;font-size:11px;">{r['article']} ยท {r['color']} ยท {r['gender']}</span>
 
 
 
 
82
  </div>"""
83
- html += "</div>"
84
- return html
85
 
 
 
 
 
 
 
 
86
 
87
- # โ”€โ”€ Gradio handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
88
- def text_search(text, category, gender, diversity):
89
- if not text.strip():
90
- return None, None, None, "<p style='color:red'>โš ๏ธ Please enter a search query.</p>"
91
- r = get_recommendations(text, category, gender, diversity)
92
- imgs = [x["image"] for x in r]
93
- while len(imgs) < 3: imgs.append(None)
94
- return imgs[0], imgs[1], imgs[2], build_info_html(r)
95
 
 
 
 
 
 
96
 
97
- def image_search(image, category, gender, diversity):
98
- if image is None:
99
- return None, None, None, "<p style='color:red'>โš ๏ธ Please upload an image.</p>"
100
- r = get_recommendations(image, category, gender, diversity)
101
- imgs = [x["image"] for x in r]
102
- while len(imgs) < 3: imgs.append(None)
103
- return imgs[0], imgs[1], imgs[2], build_info_html(r)
 
 
 
 
 
 
 
 
 
 
 
 
104
 
 
 
 
 
 
 
 
 
105
 
106
- # โ”€โ”€ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
- with gr.Blocks(theme=gr.themes.Soft(), title="Fashion Visual Search") as demo:
108
 
109
- gr.Markdown("""
110
- # ๐Ÿ‘— Fashion Visual Search
111
- **Find similar fashion items using AI-powered visual search**
112
-
113
- Describe what you're looking for *or* upload a photo of a fashion item.
114
- Powered by **CLIP** (OpenAI) ยท Cosine Similarity ยท 10,000 indexed products
115
- """)
 
 
 
 
 
 
 
 
116
 
117
- with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- # โ”€โ”€ LEFT: Inputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
120
- with gr.Column(scale=1, min_width=300):
121
 
122
- with gr.Tab("๐Ÿ“ Text Search"):
123
- text_input = gr.Textbox(
124
- placeholder="e.g. 'elegant women black handbag'",
125
- label="Describe the item you want",
126
- lines=2
127
- )
128
- text_btn = gr.Button("๐Ÿ” Search", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
- with gr.Tab("๐Ÿ–ผ๏ธ Image Search"):
131
- image_input = gr.Image(type="pil", label="Upload a fashion item photo")
132
- image_btn = gr.Button("๐Ÿ” Find Similar Items", variant="primary", size="lg")
133
 
134
- with gr.Accordion("โš™๏ธ Filters & Options", open=False):
135
- category_dd = gr.Dropdown(CATEGORIES, value="All", label="Category")
136
- gender_dd = gr.Dropdown(GENDERS, value="All", label="Gender")
137
- diversity_cb = gr.Checkbox(label="๐ŸŽจ Diversity mode (MMR)", value=False,
138
- info="Returns more varied results")
 
139
 
140
- # โ”€โ”€ RIGHT: Outputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  with gr.Column(scale=2):
142
- gr.Markdown("### ๐ŸŽฏ Top 3 Recommendations")
 
 
 
 
143
  with gr.Row():
144
- out1 = gr.Image(label="Match #1", show_label=True)
145
- out2 = gr.Image(label="Match #2", show_label=True)
146
- out3 = gr.Image(label="Match #3", show_label=True)
147
- info_out = gr.HTML()
148
-
149
- # โ”€โ”€ Examples โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
150
- gr.Examples(
151
- examples=[
152
- ["elegant women black handbag"],
153
- ["blue casual men shirt"],
154
- ["sports running shoes"],
155
- ["gold women watch"],
156
- ["red summer dress women"],
157
- ],
158
- inputs=text_input,
159
- label="๐Ÿ’ก Try these examples (click to load, then Search)"
160
- )
161
 
162
- gr.Markdown("""
163
- ---
164
- **How it works:** Your query is converted to a 512-dimensional CLIP embedding vector.
165
- Cosine similarity is then computed against all 10,000 indexed products to find the closest matches.
166
- """)
167
-
168
- # โ”€โ”€ Wire up โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
169
- text_btn.click(
170
- fn=text_search,
171
- inputs=[text_input, category_dd, gender_dd, diversity_cb],
172
- outputs=[out1, out2, out3, info_out]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  )
174
- image_btn.click(
175
- fn=image_search,
176
- inputs=[image_input, category_dd, gender_dd, diversity_cb],
177
- outputs=[out1, out2, out3, info_out]
178
  )
179
 
180
- demo.launch()
 
 
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 โ€” detects legally-ambiguous fields)
9
+ โ†’ deterministic complete() (VAT ยท serial ยท allocation number)
10
+ โ†’ fiscal document display
11
+ โ†’ FAISS recommender (3 similar past receipts)
12
+
13
+ Repos used:
14
+ Dataset : yonilev/Text2Receipt
15
+ Model : yonilev/Text2Receipt-parser (LoRA adapter + embeddings)
16
+ Space : yonilev/Text2Receipt
17
+ """
18
+ import os, json, re, random
19
  import numpy as np
20
+ import gradio as gr
21
+
22
+ # โ”€โ”€ repo constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
23
+ DATASET_REPO = "yonilev/Text2Receipt"
24
+ MODEL_REPO = "yonilev/Text2Receipt-parser"
25
+ BASE_MODEL = os.environ.get("BASE_MODEL", "unsloth/gemma-2-2b-it")
26
+
27
+ import t2r_core as core
28
+
29
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
30
+ # 1. Lazy init โ€” load heavy resources once on first request
31
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
32
+ _STATE: dict = {"ready": False}
33
+
34
+ def _lazy_init():
35
+ if _STATE["ready"]:
36
+ return
37
+ import torch
38
+ from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
39
+ from peft import PeftModel
40
+ from sentence_transformers import SentenceTransformer
41
+ import faiss
42
+ from huggingface_hub import hf_hub_download
43
+ import pandas as pd
44
+
45
+ device = "cuda" if torch.cuda.is_available() else "cpu"
46
+ _STATE["device"] = device
47
+ _STATE["rng"] = random.Random(42)
48
+
49
+ # โ”€โ”€ parser model โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
50
+ bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
51
+ bnb_4bit_compute_dtype=torch.float16,
52
+ bnb_4bit_use_double_quant=True)
53
+ tok = AutoTokenizer.from_pretrained(BASE_MODEL)
54
+ if tok.pad_token is None:
55
+ tok.pad_token = tok.eos_token
56
+ tok.padding_side = "left"
57
+
58
+ try:
59
+ base = AutoModelForCausalLM.from_pretrained(
60
+ BASE_MODEL, quantization_config=bnb, device_map="auto",
61
+ torch_dtype=torch.float16, attn_implementation="eager")
62
+ model = PeftModel.from_pretrained(base, MODEL_REPO)
63
+ print("โœ… loaded fine-tuned adapter from", MODEL_REPO)
64
+ except Exception as e:
65
+ print(f"โš  adapter load failed ({e}); falling back to base model")
66
+ model = AutoModelForCausalLM.from_pretrained(
67
+ BASE_MODEL, quantization_config=bnb, device_map="auto",
68
+ torch_dtype=torch.float16, attn_implementation="eager")
69
+
70
+ model.eval()
71
+ _STATE["tok"] = tok
72
+ _STATE["model"] = model
73
+
74
+ # โ”€โ”€ embeddings + FAISS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
75
+ emb_path = hf_hub_download(MODEL_REPO, "receipts_embeddings.npy", repo_type="model")
76
+ store_path = hf_hub_download(MODEL_REPO, "receipts_store.parquet", repo_type="model")
77
+ manifest_path = hf_hub_download(MODEL_REPO, "embeddings_manifest.json", repo_type="model")
78
+
79
+ emb = np.load(emb_path).astype("float32")
80
+ store = pd.read_parquet(store_path)
81
+ with open(manifest_path) as f:
82
+ manifest = json.load(f)
83
+
84
+ faiss.normalize_L2(emb)
85
+ index = faiss.IndexFlatIP(emb.shape[1])
86
+ index.add(emb)
87
+
88
+ enc = SentenceTransformer(manifest["embed_model"],
89
+ device=device)
90
+ _STATE["enc"] = enc
91
+ _STATE["index"] = index
92
+ _STATE["store"] = store
93
+ _STATE["e5_family"] = manifest.get("e5_family", False)
94
+ _STATE["ready"] = True
95
+ print("โœ… FAISS index ready โ€”", emb.shape[0], "vectors")
96
+
97
+
98
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
99
+ # 2. Parser helpers
100
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
101
+ INSTRUCTION = (
102
+ "ืืชื” ืžืžื™ืจ ื”ืขืจืช ื”ื›ื ืกื” ื—ื•ืคืฉื™ืช ื‘ืขื‘ืจื™ืช ืœืžื‘ื ื” JSON. "
103
+ "ื—ืœืฅ ืืš ื•ืจืง ืืช ืžื” ืฉื›ืชื•ื‘ ื‘ื”ืขืจื”: ืฉื ื”ืœืงื•ื— (client_name), "
104
+ "ื”ืื ื”ืœืงื•ื— ืขืกืง (client_is_business), ื•ืจืฉื™ืžืช ืคืจื™ื˜ื™ื (items) "
105
+ "ื›ืืฉืจ ืœื›ืœ ืคืจื™ื˜ ืชื™ืื•ืจ (description), ืžื—ื™ืจ ืœื™ื—ื™ื“ื” (unit_price) ื•ื›ืžื•ืช (quantity). "
106
+ "ื”ื—ื–ืจ JSON ืชืงื™ืŸ ื‘ืœื‘ื“, ืœืœื ื˜ืงืกื˜ ื ื•ืกืฃ."
107
+ )
108
+
109
+ def _build_prompt(raw_text: str) -> str:
110
+ return f"{INSTRUCTION}\n\nื”ืขืจื”: {raw_text}\n\nJSON:"
111
+
112
+ def _extract_json(text: str) -> dict | None:
113
+ s = text.find("{")
114
+ if s < 0:
115
+ return None
116
+ depth = 0
117
+ for i in range(s, len(text)):
118
+ if text[i] == "{": depth += 1
119
+ elif text[i] == "}":
120
+ depth -= 1
121
+ if depth == 0:
122
+ try: return json.loads(text[s:i+1])
123
+ except: return None
124
+ return None
125
+
126
+ def model_parse(raw_text: str) -> dict | None:
127
+ """Run fine-tuned model; return parse dict or None on failure."""
128
+ tok, model = _STATE["tok"], _STATE["model"]
129
+ msgs = [{"role": "user", "content": _build_prompt(raw_text)}]
130
+ prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
131
+ enc = tok(prompt, return_tensors="pt",
132
+ add_special_tokens=False).to(model.device)
133
+ import torch
134
+ with torch.no_grad():
135
+ out = model.generate(**enc, max_new_tokens=200,
136
+ do_sample=False,
137
+ pad_token_id=tok.pad_token_id)
138
+ decoded = tok.decode(out[0, enc["input_ids"].shape[1]:],
139
+ skip_special_tokens=True)
140
+ return _extract_json(decoded)
141
+
142
+
143
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
144
+ # 3. Agentic clarification logic (+5% bonus)
145
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
146
+ import datetime as _dt
147
+
148
+ def _today() -> _dt.date:
149
+ return _dt.date.today()
150
+
151
+ def _missing_fields(parse: dict | None) -> list[str]:
152
+ """Return list of missing mandatory fields that affect the document legally."""
153
+ missing = []
154
+ if parse is None:
155
+ return ["parse_failed"]
156
+ if not parse.get("client_name", "").strip():
157
+ missing.append("client_name")
158
+ if not parse.get("items"):
159
+ missing.append("items")
160
+ # doc_type not extractable from notes โ€” always needs clarification for allocation
161
+ return missing
162
+
163
+ def _needs_allocation_clarification(parse: dict) -> bool:
164
+ """Return True when the doc_type choice changes whether allocation is required."""
165
+ if parse is None:
166
+ return False
167
+ subtotal = sum(
168
+ it.get("unit_price", 0) * it.get("quantity", 1)
169
+ for it in parse.get("items", [])
170
+ )
171
+ threshold = core.allocation_threshold_for_date(_today())
172
+ is_biz = parse.get("client_is_business", False)
173
+ # allocation only matters for authorized dealer + biz client + subtotal โ‰ฅ threshold
174
+ # but we don't know issuer status here; ask when subtotal is near or above threshold
175
+ return bool(is_biz) and subtotal >= threshold * 0.8 # 80% buffer
176
+
177
+ def clarification_questions(parse: dict | None, answers: dict) -> list[str]:
178
+ """
179
+ Agentic loop: generate next question(s) given current parse + prior answers.
180
+ Returns [] when all necessary info is available.
181
+ """
182
+ questions = []
183
+ missing = _missing_fields(parse)
184
+
185
+ if "parse_failed" in missing:
186
+ questions.append("ืœื ื”ืฆืœื—ืชื™ ืœื—ืœืฅ ืคืจื˜ื™ื. ื”ืื ืชื•ื›ืœ ืœื ืกื— ืžื—ื“ืฉ? ืœื“ื•ื’ืžื”: 'ืงื™ื‘ืœืชื™ 500โ‚ช ืž[ืฉื] ืขืœ [ืฉื™ืจื•ืช]'")
187
+ return questions
188
+
189
+ if "client_name" in missing and "client_name" not in answers:
190
+ questions.append("ืžื” ืฉื ื”ืœืงื•ื—?")
191
+ if "items" in missing and "items" not in answers:
192
+ questions.append("ืžื” ื”ืฉื™ืจื•ืช/ื”ืžื•ืฆืจ ืฉืกื•ืคืง ื•ื‘ืื™ื–ื” ืžื—ื™ืจ?")
193
+
194
+ # ask doc_type only if not already answered and allocation is relevant
195
+ if "doc_type" not in answers:
196
+ if parse and _needs_allocation_clarification(parse):
197
+ questions.append(
198
+ "ื”ืื ืœื”ื ืคื™ืง ื—ืฉื‘ื•ื ื™ืช ืžืก/ืงื‘ืœื” (ืœืขืกืง ืขื ืžืข\"ืž) ืื• ืงื‘ืœื” ืคืฉื•ื˜ื”?"
199
+ )
200
+ elif parse and not parse.get("doc_type"):
201
+ questions.append(
202
+ "ืื™ื–ื” ืžืกืžืš ืœื™ื™ืฆืจ? ืงื‘ืœื” / ื—ืฉื‘ื•ื ื™ืช ืžืก / ื—ืฉื‘ื•ื ื™ืช ืžืก ื•ืงื‘ืœื”?"
203
+ )
204
 
205
+ return questions[:3] # max 3 questions per round
206
 
 
 
 
207
 
208
+ def apply_answers(parse: dict | None, answers: dict) -> dict:
209
+ """Merge user answers into parse dict."""
210
+ if parse is None:
211
+ parse = {}
212
+ p = dict(parse)
213
+ if "client_name" in answers:
214
+ p["client_name"] = answers["client_name"]
215
+ if "doc_type" in answers:
216
+ raw = answers["doc_type"].strip()
217
+ if "ืžืก/ืงื‘ืœื”" in raw or "ืžืก ื•ืงื‘ืœื”" in raw:
218
+ p["doc_type"] = "tax_invoice_receipt"
219
+ elif "ืžืก" in raw:
220
+ p["doc_type"] = "tax_invoice"
221
+ else:
222
+ p["doc_type"] = "receipt"
223
+ # set safe defaults for missing required fields
224
+ p.setdefault("doc_type", "receipt")
225
+ p.setdefault("date", _today().isoformat())
226
+ p.setdefault("payment_method", "bank_transfer")
227
+ p.setdefault("amount_basis", "net")
228
+ p.setdefault("currency", "ILS")
229
+ p.setdefault("client_tax_id", None)
230
+ p.setdefault("client_is_business", False)
231
+ p.setdefault("items", [])
232
+ return p
233
 
 
 
234
 
235
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
236
+ # 4. FAISS recommender
237
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
238
+ def recommend(query_text: str, k: int = 3) -> list[dict]:
239
+ """Return k most similar receipts from the full corpus store."""
240
+ enc = _STATE["enc"]
241
+ index = _STATE["index"]
242
+ store = _STATE["store"]
243
+ pref = "query: " if _STATE["e5_family"] else ""
244
+ q_emb = enc.encode([pref + query_text],
245
+ normalize_embeddings=True).astype("float32")
246
+ _, I = index.search(q_emb, k + 1) # +1 in case query is in corpus
247
  results = []
248
+ for idx in I[0][:k]:
249
+ row = store.iloc[int(idx)]
 
250
  results.append({
251
+ "raw_text": row["raw_text"],
252
+ "category": row["category"],
253
+ "doc_type_he": row["doc_type_he"],
254
+ "total": float(row["total"]),
255
+ "client_name": row["client_name"],
 
256
  })
257
  return results
258
 
259
 
260
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
261
+ # 5. Document renderer (HTML card)
262
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
263
+ def render_document(completed: dict) -> str:
264
+ c = completed
265
+ issuer = c.get("issuer", {})
266
+ client = c.get("client", {})
267
+ lines = c.get("lines", [])
268
+
269
+ alloc_html = ""
270
+ if c.get("allocation_required"):
271
+ alloc_html = f"""
272
+ <div class="alloc-badge">
273
+ ๐Ÿ”– ืžืกืคืจ ื”ืงืฆืื”: <strong>{c.get('allocation_number','โ€”')}</strong>
274
  </div>"""
 
 
275
 
276
+ line_rows = "".join(
277
+ f"<tr><td>{ln['description']}</td>"
278
+ f"<td>{ln['quantity']}</td>"
279
+ f"<td>โ‚ช{ln['unit_price']:,.2f}</td>"
280
+ f"<td>โ‚ช{ln['line_total']:,.2f}</td></tr>"
281
+ for ln in lines
282
+ )
283
 
284
+ vat_pct = int(round(c.get("vat_rate", 0) * 100))
 
 
 
 
 
 
 
285
 
286
+ return f"""
287
+ <div dir="rtl" style="font-family:'Arial Hebrew',Arial,sans-serif;
288
+ background:#0f1117; color:#e8eaf6; border-radius:16px;
289
+ padding:28px 32px; max-width:680px; margin:0 auto;
290
+ border:1px solid #2a2d3e; box-shadow:0 4px 32px rgba(0,0,0,.4);">
291
 
292
+ <!-- header -->
293
+ <div style="display:flex;justify-content:space-between;align-items:flex-start;
294
+ border-bottom:2px solid #0d9488;padding-bottom:16px;margin-bottom:20px;">
295
+ <div>
296
+ <div style="font-size:22px;font-weight:700;color:#0d9488;">
297
+ {c.get('doc_type_he','ืžืกืžืš')}
298
+ </div>
299
+ <div style="font-size:13px;color:#9e9e9e;margin-top:4px;">
300
+ ืžืกืณ {c.get('serial_number','')} &nbsp;|&nbsp; {c.get('issue_date','')}
301
+ </div>
302
+ </div>
303
+ <div style="text-align:left;">
304
+ <div style="font-size:15px;font-weight:600;">{issuer.get('name','')}</div>
305
+ <div style="font-size:12px;color:#9e9e9e;">ื—.ืค. {issuer.get('tax_id','')}</div>
306
+ <div style="font-size:12px;color:#9e9e9e;">
307
+ {'ืขื•ืกืง ืžื•ืจืฉื”' if issuer.get('status')=='authorized_dealer' else 'ืขื•ืกืง ืคื˜ื•ืจ'}
308
+ </div>
309
+ </div>
310
+ </div>
311
 
312
+ <!-- client -->
313
+ <div style="background:#1a1d2e;border-radius:10px;padding:14px 18px;
314
+ margin-bottom:20px;font-size:14px;">
315
+ <span style="color:#9e9e9e;">ืœืงื•ื—: </span>
316
+ <strong>{client.get('name','')}</strong>
317
+ {'<span style="margin-right:10px;font-size:12px;color:#9e9e9e;">ืขืกืง</span>' if client.get('is_business') else ''}
318
+ {f'<span style="font-size:12px;color:#9e9e9e;"> ยท ื—.ืค. {client.get("tax_id","")}</span>' if client.get('tax_id') else ''}
319
+ </div>
320
 
321
+ {alloc_html}
 
322
 
323
+ <!-- line items -->
324
+ <table style="width:100%;border-collapse:collapse;font-size:14px;
325
+ margin-bottom:18px;">
326
+ <thead>
327
+ <tr style="color:#0d9488;border-bottom:1px solid #2a2d3e;text-align:right;">
328
+ <th style="padding:6px 0;">ืฉื™ืจื•ืช/ืžื•ืฆืจ</th>
329
+ <th style="padding:6px 8px;">ื›ืžื•ืช</th>
330
+ <th style="padding:6px 8px;">ืžื—ื™ืจ ื™ื—ื™ื“ื”</th>
331
+ <th style="padding:6px 0;">ืกื”"ื› ืฉื•ืจื”</th>
332
+ </tr>
333
+ </thead>
334
+ <tbody style="color:#e8eaf6;">
335
+ {line_rows}
336
+ </tbody>
337
+ </table>
338
 
339
+ <!-- totals -->
340
+ <div style="border-top:1px solid #2a2d3e;padding-top:14px;
341
+ font-size:14px;text-align:left;">
342
+ <div style="display:flex;justify-content:space-between;margin-bottom:4px;">
343
+ <span style="color:#9e9e9e;">ืกื›ื•ื ืœืคื ื™ ืžืข"ืž</span>
344
+ <span>โ‚ช{c.get('subtotal',0):,.2f}</span>
345
+ </div>
346
+ <div style="display:flex;justify-content:space-between;margin-bottom:4px;">
347
+ <span style="color:#9e9e9e;">ืžืข"ืž {vat_pct}%</span>
348
+ <span>โ‚ช{c.get('vat_amount',0):,.2f}</span>
349
+ </div>
350
+ <div style="display:flex;justify-content:space-between;
351
+ font-size:18px;font-weight:700;color:#0d9488;margin-top:8px;">
352
+ <span>ืกื”"ื› ืœืชืฉืœื•ื</span>
353
+ <span>โ‚ช{c.get('total',0):,.2f}</span>
354
+ </div>
355
+ <div style="font-size:12px;color:#9e9e9e;margin-top:8px;text-align:right;">
356
+ ืืžืฆืขื™ ืชืฉืœื•ื: {core.PAYMENT_HE.get(c.get('payment_method',''),'โ€”')}
357
+ </div>
358
+ </div>
359
+ </div>"""
360
 
 
 
361
 
362
+ def render_recommendations(recs: list[dict]) -> str:
363
+ if not recs:
364
+ return ""
365
+ cards = []
366
+ for r in recs:
367
+ cards.append(f"""
368
+ <div style="background:#1a1d2e;border-radius:10px;padding:14px 16px;
369
+ border:1px solid #2a2d3e;font-size:13px;">
370
+ <div style="color:#0d9488;font-weight:600;margin-bottom:6px;">
371
+ {r['doc_type_he']} ยท {r['category']}
372
+ </div>
373
+ <div style="color:#e8eaf6;margin-bottom:4px;line-height:1.4;">
374
+ {r['raw_text'][:80]}{'โ€ฆ' if len(r['raw_text'])>80 else ''}
375
+ </div>
376
+ <div style="color:#9e9e9e;">
377
+ {r['client_name']} ยท โ‚ช{r['total']:,.0f}
378
+ </div>
379
+ </div>""")
380
+
381
+ return f"""
382
+ <div dir="rtl" style="font-family:Arial,sans-serif;max-width:680px;margin:16px auto 0;">
383
+ <div style="font-size:14px;color:#9e9e9e;margin-bottom:10px;">
384
+ ๐Ÿ” 3 ืงื‘ืœื•ืช ื“ื•ืžื•ืช ืžื”ืžืื’ืจ
385
+ </div>
386
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
387
+ {''.join(cards)}
388
+ </div>
389
+ </div>"""
390
+
391
+
392
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
393
+ # 6. Gradio UI
394
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
395
+ CSS = """
396
+ :root {
397
+ --bg: #0b0b0f;
398
+ --panel: #111118;
399
+ --border: #1e2130;
400
+ --teal: #0d9488;
401
+ --text: #e8eaf6;
402
+ --muted: #9e9e9e;
403
+ }
404
+ html, body, .gradio-container {
405
+ background: var(--bg) !important;
406
+ color: var(--text);
407
+ font-family: Arial, 'Arial Hebrew', sans-serif;
408
+ }
409
+ .gr-button-primary {
410
+ background: var(--teal) !important;
411
+ border: none !important;
412
+ }
413
+ textarea, input[type=text] {
414
+ background: var(--panel) !important;
415
+ color: var(--text) !important;
416
+ border: 1px solid var(--border) !important;
417
+ border-radius: 8px !important;
418
+ }
419
+ .chat-bubble-user { background: #1a2a3a !important; }
420
+ .chat-bubble-bot { background: #1a1d2e !important; }
421
+ footer { display: none !important; }
422
+ """
423
+
424
+ QUICK_STARTERS = [
425
+ ("๐Ÿ’ผ ืขื•ืกืง ืžื•ืจืฉื” + ืœืงื•ื— ืขืกืงื™ ื’ื“ื•ืœ",
426
+ "ื—ื‘ืจืช ืžืขื’ืœ ื‘ืข\"ืž ืฉื™ืœืžื” 36,500 ืฉ\"ื— ืขืœ ืคืจื•ื™ืงื˜ ืืชืจ ืžืœื. ื”ืขื‘ืจื” ื‘ื ืงืื™ืช."),
427
+ ("๐Ÿงพ ืขื•ืกืง ืคื˜ื•ืจ - ืฉื™ืขื•ืจ ืคืจื˜ื™",
428
+ "ืงื™ื‘ืœืชื™ 220 ืฉืงืœ ืžื“ื‘ื™ืจ ืขืœ ืฉื™ืขื•ืจ ืฉื—ื™ื™ื” ืคืจื˜ื™, ืžื–ื•ืžืŸ"),
429
+ ("๐Ÿ“ฆ ื—ืฉื‘ื•ื ื™ืช ืžืก - ื›ืžื” ืคืจื™ื˜ื™ื",
430
+ "ื ื•ืขื” ื›ื”ืŸ ืฉื™ืœืžื” 2 * 350 ืฉ\"ื— ื™ื™ืขื•ืฅ ืขืกืงื™ + 180 ืฉ\"ื— ื”ื›ื ืช ืžืฆื’ืช"),
431
+ ]
432
+
433
+ DEMO_ISSUER = {
434
+ "name": "ืžื™ื ืฉืงื˜ื™ื",
435
+ "tax_id": "962569844",
436
+ "status": "authorized_dealer",
437
+ }
438
+
439
+ def _run_pipeline(raw_text: str, chat_history: list,
440
+ pending_parse: dict | None, answers: dict):
441
+ """
442
+ Main pipeline step.
443
+ Returns: (chat_history, doc_html, recs_html, pending_parse, answers)
444
+ """
445
+ _lazy_init()
446
+
447
+ # Step 1 โ€” parse
448
+ if pending_parse is None:
449
+ parse = model_parse(raw_text)
450
+ else:
451
+ parse = pending_parse
452
+
453
+ # Step 2 โ€” check for questions
454
+ questions = clarification_questions(parse, answers)
455
+ if questions:
456
+ q_text = "\n".join(f"โ€ข {q}" for q in questions)
457
+ chat_history = chat_history + [
458
+ {"role": "assistant",
459
+ "content": f"ื™ืฉ ืœื™ ื›ืžื” ืฉืืœื•ืช ืœืคื ื™ ืฉืืคื™ืง ืืช ื”ืžืกืžืš:\n\n{q_text}"}
460
+ ]
461
+ return chat_history, "", "", parse, answers
462
+
463
+ # Step 3 โ€” apply answers + complete
464
+ final_parse = apply_answers(parse, answers)
465
+ try:
466
+ completed = core.complete(DEMO_ISSUER, final_parse, _STATE["rng"])
467
+ except Exception as e:
468
+ return (chat_history + [{"role": "assistant",
469
+ "content": f"ืฉื’ื™ืื” ื‘ืขื™ื‘ื•ื“: {e}"}],
470
+ "", "", None, {})
471
+
472
+ # Step 4 โ€” render
473
+ doc_html = render_document(completed)
474
+ recs = recommend(raw_text)
475
+ recs_html = render_recommendations(recs)
476
+
477
+ chat_history = chat_history + [
478
+ {"role": "assistant",
479
+ "content": "โœ… ื”ืžืกืžืš ื”ื•ืคืง ื‘ื”ืฆืœื—ื”! ืจืื” ืœืžื˜ื”."}
480
+ ]
481
+ return chat_history, doc_html, recs_html, None, {}
482
 
 
 
 
483
 
484
+ def _handle_answer(user_msg: str, chat_history: list,
485
+ pending_parse: dict | None, answers: dict,
486
+ raw_text: str):
487
+ """Parse a free-text answer and route back through pipeline."""
488
+ a = dict(answers)
489
+ low = user_msg.strip()
490
 
491
+ # simple intent detection for clarification answers
492
+ if any(w in low for w in ["ื—ืฉื‘ื•ื ื™ืช ืžืก", "ืžืก ื•ืงื‘ืœื”", "ืžืก/ืงื‘ืœื”"]):
493
+ a["doc_type"] = low
494
+ elif "ืงื‘ืœื”" in low:
495
+ a["doc_type"] = "receipt"
496
+ elif not a.get("client_name") and pending_parse and not pending_parse.get("client_name"):
497
+ a["client_name"] = user_msg.strip()
498
+
499
+ chat_history = chat_history + [{"role": "user", "content": user_msg}]
500
+ return _run_pipeline(raw_text, chat_history, pending_parse, a)
501
+
502
+
503
+ # โ”€โ”€ build UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
504
+ with gr.Blocks(css=CSS, title="Text2Receipt") as demo:
505
+
506
+ gr.HTML("""
507
+ <div dir="rtl" style="text-align:center;padding:24px 0 8px;">
508
+ <div style="font-size:36px;margin-bottom:6px;">๐Ÿงพ</div>
509
+ <div style="font-size:28px;font-weight:800;color:#0d9488;">Text2Receipt</div>
510
+ <div style="font-size:14px;color:#9e9e9e;margin-top:4px;">
511
+ ื”ืขืจืช ื”ื›ื ืกื” ื—ื•ืคืฉื™ืช ื‘ืขื‘ืจื™ืช โ†’ ืžืกืžืš ืคื™ืกืงืœื™ ื™ืฉืจืืœื™ ืชืงื™ืŸ
512
+ </div>
513
+ </div>""")
514
+
515
+ # state
516
+ st_parse = gr.State(None)
517
+ st_answers = gr.State({})
518
+ st_raw = gr.State("")
519
+
520
+ with gr.Row():
521
  with gr.Column(scale=2):
522
+ note_input = gr.Textbox(
523
+ label="ื”ืขืจืช ื”ื›ื ืกื” (ืขื‘ืจื™ืช ื—ื•ืคืฉื™ืช)",
524
+ placeholder="ืœื“ื•ื’ืžื”: ืงื™ื‘ืœืชื™ 1,200 ืฉ\"ื— ืžืžืฉื” ื›ื”ืŸ ืขืœ ื™ื™ืขื•ืฅ ืขืกืงื™",
525
+ lines=3, rtl=True,
526
+ )
527
  with gr.Row():
528
+ submit_btn = gr.Button("โšก ื”ืคืง ืžืกืžืš", variant="primary")
529
+ clear_btn = gr.Button("๐Ÿ—‘ ื ืงื”")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
+ gr.Markdown("**ื“ื•ื’ืžืื•ืช ืžื”ื™ืจื•ืช:**", rtl=True)
532
+ for label, example in QUICK_STARTERS:
533
+ gr.Button(label).click(
534
+ fn=lambda ex=example: ex,
535
+ outputs=note_input
536
+ )
537
+
538
+ with gr.Column(scale=3):
539
+ chatbot = gr.Chatbot(
540
+ label="ืกื•ื›ืŸ ื”ื‘ื”ืจื”",
541
+ type="messages",
542
+ rtl=True,
543
+ height=220,
544
+ )
545
+ answer_input = gr.Textbox(
546
+ label="ืชืฉื•ื‘ื” ืœืฉืืœืช ื”ืกื•ื›ืŸ",
547
+ placeholder="ื”ืงืœื“ ืชืฉื•ื‘ื” ื•ืœื—ืฅ Enter...",
548
+ visible=True, rtl=True,
549
+ )
550
+
551
+ doc_output = gr.HTML(label="ืžืกืžืš ืคื™ืกืงืœื™")
552
+ recs_output = gr.HTML(label="ืงื‘ืœื•ืช ื“ื•ืžื•ืช")
553
+
554
+ # โ”€โ”€ event wiring โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
555
+ def on_submit(raw, history, pending, answers):
556
+ history = history + [{"role": "user", "content": raw}]
557
+ return _run_pipeline(raw, history, None, {})
558
+
559
+ submit_btn.click(
560
+ fn=on_submit,
561
+ inputs=[note_input, chatbot, st_parse, st_answers],
562
+ outputs=[chatbot, doc_output, recs_output, st_parse, st_answers],
563
+ )
564
+ note_input.submit(
565
+ fn=on_submit,
566
+ inputs=[note_input, chatbot, st_parse, st_answers],
567
+ outputs=[chatbot, doc_output, recs_output, st_parse, st_answers],
568
+ )
569
+ answer_input.submit(
570
+ fn=_handle_answer,
571
+ inputs=[answer_input, chatbot, st_parse, st_answers, note_input],
572
+ outputs=[chatbot, doc_output, recs_output, st_parse, st_answers],
573
  )
574
+ clear_btn.click(
575
+ fn=lambda: ([], "", "", None, {}, ""),
576
+ outputs=[chatbot, doc_output, recs_output, st_parse, st_answers, note_input],
 
577
  )
578
 
579
+ if __name__ == "__main__":
580
+ demo.launch()