Update app.py
Browse files
app.py
CHANGED
|
@@ -1,329 +1,109 @@
|
|
| 1 |
-
# app.py
|
| 2 |
import gradio as gr
|
| 3 |
-
import pandas as pd
|
| 4 |
import pytesseract
|
| 5 |
-
import
|
|
|
|
| 6 |
import re
|
| 7 |
-
import tempfile
|
| 8 |
-
import shutil
|
| 9 |
import os
|
| 10 |
-
import
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
"
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
else:
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
branch_name = ""
|
| 52 |
-
return store_name, store_code, branch_name
|
| 53 |
|
| 54 |
-
|
| 55 |
-
gray = cv2.cvtColor(np_img, cv2.COLOR_RGB2GRAY)
|
| 56 |
-
h, w = gray.shape[:2]
|
| 57 |
-
if min(h, w) < 1000:
|
| 58 |
-
scale = max(1.5, 1000.0 / min(h, w))
|
| 59 |
-
gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
|
| 60 |
-
th = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
| 61 |
-
cv2.THRESH_BINARY, 41, 11)
|
| 62 |
-
kernel = np.ones((1, 1), np.uint8)
|
| 63 |
-
opened = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel)
|
| 64 |
-
return opened
|
| 65 |
|
| 66 |
-
def
|
| 67 |
-
#
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
lines = []
|
| 71 |
-
if data.empty:
|
| 72 |
-
return "", []
|
| 73 |
-
data['line_key'] = data['block_num'].astype(str) + "_" + data['par_num'].astype(str) + "_" + data['line_num'].astype(str)
|
| 74 |
-
for key, grp in data.groupby('line_key'):
|
| 75 |
-
text = " ".join(grp['text'].astype(str).tolist()).strip()
|
| 76 |
-
confs = grp['conf'].astype(float)
|
| 77 |
-
confs = confs[confs >= 0]
|
| 78 |
-
avg_conf = float(confs.mean()) if not confs.empty else 0.0
|
| 79 |
-
lines.append({"line": text, "conf": round(avg_conf, 2)})
|
| 80 |
-
full_text = "\n".join([l["line"] for l in lines])
|
| 81 |
-
return full_text, lines
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
return lines
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
if any(k in low for k in CATEGORY_HINTS):
|
| 92 |
-
return True
|
| 93 |
-
if not re.search(r"\d", line) and len(line.split()) <= 6:
|
| 94 |
-
return True
|
| 95 |
-
return False
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
if looks_like_category(ln):
|
| 103 |
-
if ln.isupper() or any(k in ln.lower() for k in CATEGORY_HINTS):
|
| 104 |
-
current_parent = ln.strip(":- ")
|
| 105 |
-
continue
|
| 106 |
-
else:
|
| 107 |
-
current_category = ln.strip(":- ")
|
| 108 |
-
continue
|
| 109 |
-
m = PRICE_REGEX.search(ln)
|
| 110 |
-
if m:
|
| 111 |
-
price = m.group(1).strip()
|
| 112 |
-
name_part = PRICE_REGEX.sub("", ln).strip(" -:.")
|
| 113 |
-
row = {
|
| 114 |
-
"Parent Category": current_parent,
|
| 115 |
-
"Category": current_category,
|
| 116 |
-
"Name": name_part,
|
| 117 |
-
"Item Code": "",
|
| 118 |
-
"Master Item Name": name_part,
|
| 119 |
-
"EAN Code": "",
|
| 120 |
-
"Price": price,
|
| 121 |
-
"Active": DEFAULTS["Active"],
|
| 122 |
-
"Priority": DEFAULTS["Priority"],
|
| 123 |
-
"Image": DEFAULTS["Image"],
|
| 124 |
-
"Food type": DEFAULTS["Food type"],
|
| 125 |
-
"NoOfMains": DEFAULTS["NoOfMains"],
|
| 126 |
-
"OnlineName": DEFAULTS["OnlineName"],
|
| 127 |
-
"AlternateClassification": DEFAULTS["AlternateClassification"],
|
| 128 |
-
"ItemTaxInclusive": DEFAULTS["ItemTaxInclusive"],
|
| 129 |
-
"TaxPct": DEFAULTS["TaxPct"],
|
| 130 |
-
"BrandName": DEFAULTS["BrandName"],
|
| 131 |
-
"ClassificationCode": DEFAULTS["ClassificationCode"],
|
| 132 |
-
"HSN Code": DEFAULTS["HSN Code"]
|
| 133 |
-
}
|
| 134 |
-
rows.append(row)
|
| 135 |
else:
|
| 136 |
-
|
| 137 |
-
name_part = ln.strip()
|
| 138 |
-
row = {
|
| 139 |
-
"Parent Category": current_parent,
|
| 140 |
-
"Category": current_category,
|
| 141 |
-
"Name": name_part,
|
| 142 |
-
"Item Code": "",
|
| 143 |
-
"Master Item Name": name_part,
|
| 144 |
-
"EAN Code": "",
|
| 145 |
-
"Price": "",
|
| 146 |
-
"Active": DEFAULTS["Active"],
|
| 147 |
-
"Priority": DEFAULTS["Priority"],
|
| 148 |
-
"Image": DEFAULTS["Image"],
|
| 149 |
-
"Food type": DEFAULTS["Food type"],
|
| 150 |
-
"NoOfMains": DEFAULTS["NoOfMains"],
|
| 151 |
-
"OnlineName": DEFAULTS["OnlineName"],
|
| 152 |
-
"AlternateClassification": DEFAULTS["AlternateClassification"],
|
| 153 |
-
"ItemTaxInclusive": DEFAULTS["ItemTaxInclusive"],
|
| 154 |
-
"TaxPct": DEFAULTS["TaxPct"],
|
| 155 |
-
"BrandName": DEFAULTS["BrandName"],
|
| 156 |
-
"ClassificationCode": DEFAULTS["ClassificationCode"],
|
| 157 |
-
"HSN Code": DEFAULTS["HSN Code"]
|
| 158 |
-
}
|
| 159 |
-
rows.append(row)
|
| 160 |
-
return rows
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
ws["C1"] = branch_name
|
| 168 |
-
start_row = 3
|
| 169 |
-
r = start_row
|
| 170 |
-
for item in rows:
|
| 171 |
-
ws.cell(row=r, column=1, value=item["Parent Category"])
|
| 172 |
-
ws.cell(row=r, column=2, value=item["Category"])
|
| 173 |
-
ws.cell(row=r, column=3, value=item["Name"])
|
| 174 |
-
ws.cell(row=r, column=4, value=item["Item Code"])
|
| 175 |
-
ws.cell(row=r, column=5, value=item["Master Item Name"])
|
| 176 |
-
ws.cell(row=r, column=6, value=item["EAN Code"])
|
| 177 |
-
ws.cell(row=r, column=7, value=item["Price"])
|
| 178 |
-
ws.cell(row=r, column=8, value=item["Active"])
|
| 179 |
-
ws.cell(row=r, column=9, value=item["Priority"])
|
| 180 |
-
ws.cell(row=r, column=10, value=item["Image"])
|
| 181 |
-
ws.cell(row=r, column=11, value=item["Food type"])
|
| 182 |
-
ws.cell(row=r, column=12, value=item["NoOfMains"])
|
| 183 |
-
ws.cell(row=r, column=13, value=item["OnlineName"])
|
| 184 |
-
ws.cell(row=r, column=14, value=item["AlternateClassification"])
|
| 185 |
-
ws.cell(row=r, column=15, value=item["ItemTaxInclusive"])
|
| 186 |
-
ws.cell(row=r, column=16, value=item["TaxPct"])
|
| 187 |
-
ws.cell(row=r, column=17, value=item["BrandName"])
|
| 188 |
-
ws.cell(row=r, column=18, value=item["ClassificationCode"])
|
| 189 |
-
ws.cell(row=r, column=19, value=item["HSN Code"])
|
| 190 |
-
r += 1
|
| 191 |
-
out = BytesIO()
|
| 192 |
-
wb.save(out)
|
| 193 |
-
out.seek(0)
|
| 194 |
-
return out
|
| 195 |
|
| 196 |
-
#
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
pil = Image.open(BytesIO(raw)).convert("RGB")
|
| 203 |
-
np_img = np.array(pil)
|
| 204 |
-
pre = preprocess_image_cv(np_img)
|
| 205 |
-
pil_pre = Image.fromarray(pre)
|
| 206 |
-
full_text, lines_conf = ocr_with_confidence(pil_pre)
|
| 207 |
-
lines = split_lines(full_text)
|
| 208 |
-
rows = parse_menu_lines(lines)
|
| 209 |
-
parsed[img.name] = {
|
| 210 |
-
"store_name": store_name,
|
| 211 |
-
"store_code": store_code,
|
| 212 |
-
"branch_name": branch_name,
|
| 213 |
-
"rows": rows,
|
| 214 |
-
"raw_text": full_text,
|
| 215 |
-
"lines_conf": lines_conf
|
| 216 |
-
}
|
| 217 |
-
return parsed
|
| 218 |
|
| 219 |
-
def to_display_df(rows):
|
| 220 |
-
if not rows:
|
| 221 |
-
return pd.DataFrame(columns=["Parent Category","Category","Name","Item Code","Master Item Name","EAN Code","Price","Active","Priority","Image","Food type","NoOfMains","OnlineName","AlternateClassification","ItemTaxInclusive","TaxPct","BrandName","ClassificationCode","HSN Code"])
|
| 222 |
-
return pd.DataFrame(rows)
|
| 223 |
-
|
| 224 |
-
def save_single(parsed_item, template_file):
|
| 225 |
-
rows = parsed_item["rows"]
|
| 226 |
-
if isinstance(rows, pd.DataFrame):
|
| 227 |
-
rows = rows.to_dict(orient="records")
|
| 228 |
-
out_buf = fill_template(template_file.name, rows, parsed_item["store_name"], parsed_item["store_code"], parsed_item["branch_name"])
|
| 229 |
-
return out_buf
|
| 230 |
-
|
| 231 |
-
def download_all(parsed_state, template_file):
|
| 232 |
-
tempdir = tempfile.mkdtemp()
|
| 233 |
-
zip_path = os.path.join(tempdir, "Menu_Results.zip")
|
| 234 |
-
with ZipFile(zip_path, "w") as zf:
|
| 235 |
-
for name, item in parsed_state.items():
|
| 236 |
-
try:
|
| 237 |
-
out_buf = save_single(item, template_file)
|
| 238 |
-
out_name = os.path.splitext(os.path.basename(name))[0] + ".xlsx"
|
| 239 |
-
tmp_path = os.path.join(tempdir, out_name)
|
| 240 |
-
with open(tmp_path, "wb") as f:
|
| 241 |
-
f.write(out_buf.read())
|
| 242 |
-
zf.write(tmp_path, arcname=out_name)
|
| 243 |
-
except Exception as e:
|
| 244 |
-
err_name = os.path.splitext(os.path.basename(name))[0] + "_ERROR.txt"
|
| 245 |
-
err_path = os.path.join(tempdir, err_name)
|
| 246 |
-
with open(err_path, "w", encoding="utf-8") as ef:
|
| 247 |
-
ef.write(str(e))
|
| 248 |
-
zf.write(err_path, arcname=err_name)
|
| 249 |
return zip_path
|
| 250 |
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
gr.Markdown("# 🍽️ Menu OCR → Excel (Batch + Validation)\nUpload multiple menu images (named like <StoreName>_<StoreCode> <BranchName>.jpg) and an Excel template. Parse → review/edit each file → download ZIP of filled Excel files.")
|
| 254 |
-
with gr.Row():
|
| 255 |
-
img_input = gr.File(label="Upload Menu Images (multiple)", file_count="multiple", file_types=["image"])
|
| 256 |
-
template_input = gr.File(label="Upload Excel Template (.xlsx)", file_count="single", file_types=[".xlsx"])
|
| 257 |
-
parse_btn = gr.Button("Parse all images")
|
| 258 |
-
parsed_state = gr.State({})
|
| 259 |
-
status = gr.Textbox(label="Status", interactive=False)
|
| 260 |
with gr.Row():
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
raw_text_area = gr.Textbox(label="Raw OCR Text", lines=10)
|
| 265 |
-
conf_area = gr.Dataframe(headers=["line","confidence"], interactive=False)
|
| 266 |
-
df_editor = gr.Dataframe(headers=["Parent Category","Category","Name","Item Code","Master Item Name","EAN Code","Price","Active","Priority","Image","Food type","NoOfMains","OnlineName","AlternateClassification","ItemTaxInclusive","TaxPct","BrandName","ClassificationCode","HSN Code"], interactive=True, datatype="str")
|
| 267 |
-
with gr.Row():
|
| 268 |
-
save_btn = gr.Button("Save current edits (generate Excel for this file)")
|
| 269 |
-
save_status = gr.Textbox(label="Save status", interactive=False)
|
| 270 |
-
download_btn = gr.Button("Download ZIP of all (use after saving/edits)")
|
| 271 |
-
download_output = gr.File(label="Download ZIP")
|
| 272 |
-
|
| 273 |
-
# Callbacks
|
| 274 |
-
def do_parse(images, template):
|
| 275 |
-
if images is None or template is None:
|
| 276 |
-
return {}, "Please upload images and a template", [], ""
|
| 277 |
-
parsed = parse_uploaded(images, template)
|
| 278 |
-
choices = list(parsed.keys())
|
| 279 |
-
return parsed, f"Parsed {len(choices)} images", choices, ""
|
| 280 |
-
|
| 281 |
-
parse_btn.click(fn=do_parse, inputs=[img_input, template_input], outputs=[parsed_state, status, file_select, raw_text_area])
|
| 282 |
-
|
| 283 |
-
def refresh_choices(parsed):
|
| 284 |
-
if not parsed:
|
| 285 |
-
return [], ""
|
| 286 |
-
return list(parsed.keys()), ""
|
| 287 |
-
|
| 288 |
-
refresh_btn.click(fn=refresh_choices, inputs=[parsed_state], outputs=[file_select, status])
|
| 289 |
-
|
| 290 |
-
def show_file_details(selected, parsed):
|
| 291 |
-
if not parsed or not selected:
|
| 292 |
-
return "", pd.DataFrame(), []
|
| 293 |
-
item = parsed.get(selected)
|
| 294 |
-
raw = item.get("raw_text","")
|
| 295 |
-
lines_conf = item.get("lines_conf",[])
|
| 296 |
-
df = to_display_df(item.get("rows",[]))
|
| 297 |
-
df_conf = pd.DataFrame(lines_conf)
|
| 298 |
-
return raw, df, df_conf
|
| 299 |
-
|
| 300 |
-
file_select.change(fn=show_file_details, inputs=[file_select, parsed_state], outputs=[raw_text_area, df_editor, conf_area])
|
| 301 |
-
|
| 302 |
-
def save_current(selected, parsed, edited_df, template):
|
| 303 |
-
if not parsed or not selected:
|
| 304 |
-
return "Nothing to save"
|
| 305 |
-
item = parsed.get(selected)
|
| 306 |
-
if isinstance(edited_df, pd.DataFrame):
|
| 307 |
-
rows = edited_df.fillna("").to_dict(orient="records")
|
| 308 |
-
else:
|
| 309 |
-
rows = edited_df
|
| 310 |
-
item["rows"] = rows
|
| 311 |
-
out_buf = fill_template(template.name, rows, item["store_name"], item["store_code"], item["branch_name"])
|
| 312 |
-
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
|
| 313 |
-
tmp.write(out_buf.read())
|
| 314 |
-
tmp.close()
|
| 315 |
-
item["generated_path"] = tmp.name
|
| 316 |
-
parsed[selected] = item
|
| 317 |
-
return f"Saved {selected} -> {os.path.basename(tmp.name)}"
|
| 318 |
-
|
| 319 |
-
save_btn.click(fn=save_current, inputs=[file_select, parsed_state, df_editor, template_input], outputs=[save_status])
|
| 320 |
-
|
| 321 |
-
def do_download(parsed, template):
|
| 322 |
-
if not parsed:
|
| 323 |
-
return None
|
| 324 |
-
zip_path = download_all(parsed, template)
|
| 325 |
-
return zip_path
|
| 326 |
|
| 327 |
-
|
| 328 |
|
| 329 |
-
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
import pytesseract
|
| 3 |
+
from PIL import Image, ImageOps, ImageFilter
|
| 4 |
+
import pandas as pd
|
| 5 |
import re
|
|
|
|
|
|
|
| 6 |
import os
|
| 7 |
+
import zipfile
|
| 8 |
+
import tempfile
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
# -----------------------------
|
| 12 |
+
# Basic parsing helpers
|
| 13 |
+
# -----------------------------
|
| 14 |
+
PRICE_PATTERN = re.compile(r'(?<!\d)(?:₹\s*|Rs\.?\s*|INR\s*)?\d+(?:\.\d{1,2})?(?!\d)')
|
| 15 |
+
CLEAN_PRICE = re.compile(r'[^0-9.]')
|
| 16 |
+
|
| 17 |
+
def preprocess_image(img: Image.Image) -> Image.Image:
|
| 18 |
+
# Convert to grayscale, increase contrast, denoise lightly, sharpen
|
| 19 |
+
gray = ImageOps.grayscale(img)
|
| 20 |
+
enhanced = ImageOps.autocontrast(gray)
|
| 21 |
+
denoised = enhanced.filter(ImageFilter.MedianFilter(size=3))
|
| 22 |
+
sharpened = denoised.filter(ImageFilter.UnsharpMask(radius=1.5, percent=150, threshold=3))
|
| 23 |
+
return sharpened
|
| 24 |
+
|
| 25 |
+
def simple_parse_lines(text: str):
|
| 26 |
+
"""
|
| 27 |
+
Heuristic parser:
|
| 28 |
+
- Splits text into lines
|
| 29 |
+
- Tries to extract Item and Price from each line
|
| 30 |
+
- Category guessed from headings (lines in ALL CAPS or ending with ':')
|
| 31 |
+
"""
|
| 32 |
+
rows = []
|
| 33 |
+
current_category = None
|
| 34 |
+
|
| 35 |
+
lines = [l.strip() for l in text.splitlines() if l.strip()]
|
| 36 |
+
for line in lines:
|
| 37 |
+
# Category guess
|
| 38 |
+
if (line.isupper() and len(line.split()) <= 6) or line.endswith(':'):
|
| 39 |
+
current_category = line.rstrip(':').strip()
|
| 40 |
+
continue
|
| 41 |
+
|
| 42 |
+
# Find price
|
| 43 |
+
price_match = PRICE_PATTERN.search(line)
|
| 44 |
+
if price_match:
|
| 45 |
+
price_text = price_match.group(0)
|
| 46 |
+
price_value = CLEAN_PRICE.sub('', price_text)
|
| 47 |
+
# Item is everything before price
|
| 48 |
+
item = line[:price_match.start()].strip(" -:•\t")
|
| 49 |
+
# Cleanup item
|
| 50 |
+
item = re.sub(r'\s{2,}', ' ', item)
|
| 51 |
+
if item:
|
| 52 |
+
rows.append({
|
| 53 |
+
"Item": item,
|
| 54 |
+
"Price": price_value,
|
| 55 |
+
"Category": current_category if current_category else ""
|
| 56 |
+
})
|
| 57 |
else:
|
| 58 |
+
# If no price, optionally keep as a note; comment out if you prefer dropping
|
| 59 |
+
pass
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
return rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
def process_images_to_zip(files):
|
| 64 |
+
# Create temp workspace
|
| 65 |
+
work_dir = tempfile.mkdtemp(prefix="menu_excel_")
|
| 66 |
+
output_files = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
for idx, file_obj in enumerate(files, start=1):
|
| 69 |
+
# Load image
|
| 70 |
+
image = Image.open(file_obj.name).convert("RGB")
|
| 71 |
+
image = preprocess_image(image)
|
|
|
|
| 72 |
|
| 73 |
+
# OCR
|
| 74 |
+
text = pytesseract.image_to_string(image, lang="eng")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
+
# Parse
|
| 77 |
+
rows = simple_parse_lines(text)
|
| 78 |
+
if not rows:
|
| 79 |
+
# Fallback: dump raw text if parsing failed
|
| 80 |
+
df = pd.DataFrame([{"Extracted Text": text}])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
else:
|
| 82 |
+
df = pd.DataFrame(rows, columns=["Item", "Price", "Category"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
# Save Excel
|
| 85 |
+
excel_name = f"menu_{idx:03d}.xlsx"
|
| 86 |
+
excel_path = os.path.join(work_dir, excel_name)
|
| 87 |
+
df.to_excel(excel_path, index=False)
|
| 88 |
+
output_files.append(excel_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
+
# Bundle ZIP
|
| 91 |
+
zip_name = f"menus_output_{uuid.uuid4().hex[:8]}.zip"
|
| 92 |
+
zip_path = os.path.join(work_dir, zip_name)
|
| 93 |
+
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
| 94 |
+
for path in output_files:
|
| 95 |
+
zipf.write(path, arcname=os.path.basename(path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
return zip_path
|
| 98 |
|
| 99 |
+
with gr.Blocks(title="Menu to Excel (one file per image)") as demo:
|
| 100 |
+
gr.Markdown("## Menu to Excel converter\nUpload menu images to get a ZIP containing separate Excel files (one per image).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
with gr.Row():
|
| 102 |
+
input_files = gr.File(label="Upload menu images", file_count="multiple", type="file", file_types=[".png", ".jpg", ".jpeg"])
|
| 103 |
+
run_btn = gr.Button("Process")
|
| 104 |
+
output_zip = gr.File(label="Download ZIP")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
run_btn.click(fn=process_images_to_zip, inputs=[input_files], outputs=[output_zip])
|
| 107 |
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
demo.launch()
|