Spaces:
Sleeping
Sleeping
| # -*- coding: utf-8 -*- | |
| """ | |
| Text2Receipt β Discord Bot (+5% bonus) | |
| ======================================== | |
| Run this cell in Google Colab (T4) AFTER the fine-tuned model is loaded. | |
| The bot wraps the same pipeline as the Gradio Space. | |
| Usage in any Discord channel: | |
| !receipt <Hebrew income note> | |
| Example: | |
| !receipt Χ§ΧΧΧΧͺΧ 1200 Χ©Χ§Χ ΧΧΧ©Χ ΧΧΧ Χ’Χ ΧΧΧ’ΧΧ₯ Χ’Χ‘Χ§Χ | |
| Setup: | |
| 1. Go to https://discord.com/developers/applications β New Application | |
| 2. Bot tab β Add Bot β copy token | |
| 3. OAuth2 β URL Generator: scope=bot, permissions=Send Messages | |
| 4. Paste DISCORD_TOKEN below (or set as Colab env var) | |
| 5. Run this cell while the model is already loaded in the session | |
| """ | |
| import os, json, random, datetime as _dt, asyncio, threading | |
| import discord | |
| from discord.ext import commands | |
| # ββ same pipeline helpers as app.py ββββββββββββββββββββββββββββββββββββββββββ | |
| import t2r_core as core | |
| DEMO_ISSUER = { | |
| "name": "ΧΧΧ Χ©Χ§ΧΧΧ", | |
| "tax_id": "962569844", | |
| "status": "authorized_dealer", | |
| } | |
| _RNG = random.Random(99) | |
| # These are defined here so the bot works standalone (copied from app.py) | |
| INSTRUCTION = ( | |
| "ΧΧͺΧ ΧΧΧΧ¨ ΧΧ’Χ¨Χͺ ΧΧΧ Χ‘Χ ΧΧΧ€Χ©ΧΧͺ ΧΧ’ΧΧ¨ΧΧͺ ΧΧΧΧ Χ JSON. " | |
| "ΧΧΧ₯ ΧΧ ΧΧ¨Χ§ ΧΧͺ ΧΧ Χ©ΧΧͺΧΧ ΧΧΧ’Χ¨Χ: Χ©Χ ΧΧΧ§ΧΧ (client_name), " | |
| "ΧΧΧ ΧΧΧ§ΧΧ Χ’Χ‘Χ§ (client_is_business), ΧΧ¨Χ©ΧΧΧͺ Χ€Χ¨ΧΧΧΧ (items) " | |
| "ΧΧΧ©Χ¨ ΧΧΧ Χ€Χ¨ΧΧ ΧͺΧΧΧΧ¨ (description), ΧΧΧΧ¨ ΧΧΧΧΧΧ (unit_price) ΧΧΧΧΧͺ (quantity). " | |
| "ΧΧΧΧ¨ JSON ΧͺΧ§ΧΧ ΧΧΧΧ, ΧΧΧ ΧΧ§Χ‘Χ Χ ΧΧ‘Χ£." | |
| ) | |
| def _build_prompt(raw_text: str) -> str: | |
| return f"{INSTRUCTION}\n\nΧΧ’Χ¨Χ: {raw_text}\n\nJSON:" | |
| def _extract_json(text: str): | |
| s = text.find("{") | |
| if s < 0: | |
| return None | |
| depth = 0 | |
| for i in range(s, len(text)): | |
| if text[i] == "{": | |
| depth += 1 | |
| elif text[i] == "}": | |
| depth -= 1 | |
| if depth == 0: | |
| try: | |
| return json.loads(text[s:i+1]) | |
| except Exception: | |
| return None | |
| return None | |
| def _apply_defaults(parse: dict) -> dict: | |
| p = dict(parse) | |
| p.setdefault("doc_type", "receipt") | |
| p.setdefault("date", _dt.date.today().isoformat()) | |
| p.setdefault("payment_method", "bank_transfer") | |
| p.setdefault("amount_basis", "net") | |
| p.setdefault("currency", "ILS") | |
| p.setdefault("client_tax_id", None) | |
| p.setdefault("client_is_business", False) | |
| p.setdefault("items", []) | |
| return p | |
| def _format_document(completed: dict) -> str: | |
| """Format completed document as Discord-friendly Markdown.""" | |
| c = completed | |
| issuer = c.get("issuer", {}) | |
| client = c.get("client", {}) | |
| lines = c.get("lines", []) | |
| vat_pct = int(round(c.get("vat_rate", 0) * 100)) | |
| line_rows = "\n".join( | |
| f" β’ {ln['description']} Γ {ln['quantity']} β βͺ{ln['line_total']:,.2f}" | |
| for ln in lines | |
| ) | |
| alloc = ( | |
| f"\nπ **ΧΧ‘Χ€Χ¨ ΧΧ§Χ¦ΧΧ:** `{c.get('allocation_number')}`" | |
| if c.get("allocation_required") else "" | |
| ) | |
| return ( | |
| f"```\n" | |
| f"{'β'*38}\n" | |
| f" {c.get('doc_type_he','ΧΧ‘ΧΧ')} | ΧΧ‘Χ³ {c.get('serial_number','')}\n" | |
| f" {c.get('issue_date','')}\n" | |
| f"{'β'*38}\n" | |
| f" ΧΧ Χ€ΧΧ§ : {issuer.get('name','')} (Χ.Χ€. {issuer.get('tax_id','')})\n" | |
| f" ΧΧ§ΧΧ : {client.get('name','')}" | |
| f"{' [Χ’Χ‘Χ§]' if client.get('is_business') else ''}\n" | |
| f"{'β'*38}\n" | |
| f"{line_rows}\n" | |
| f"{'β'*38}\n" | |
| f" Χ‘ΧΧΧ Χ ΧΧ : βͺ{c.get('subtotal',0):>10,.2f}\n" | |
| f" ΧΧ’\"Χ {vat_pct:2d}% : βͺ{c.get('vat_amount',0):>10,.2f}\n" | |
| f" **Χ‘Χ\"Χ** : βͺ{c.get('total',0):>10,.2f}\n" | |
| f" ΧͺΧ©ΧΧΧ : {core.PAYMENT_HE.get(c.get('payment_method',''),'β')}\n" | |
| f"{'β'*38}\n" | |
| f"```" | |
| f"{alloc}" | |
| ) | |
| def run_pipeline(raw_text: str, tok, model) -> str: | |
| """ | |
| Full pipeline: raw Hebrew note β formatted fiscal document string. | |
| tok and model are the already-loaded objects from the Colab session. | |
| """ | |
| import torch | |
| # 1. Parse | |
| msgs = [{"role": "user", "content": _build_prompt(raw_text)}] | |
| prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True) | |
| enc = tok(prompt, return_tensors="pt", | |
| add_special_tokens=False).to(model.device) | |
| with torch.no_grad(): | |
| out = model.generate(**enc, max_new_tokens=200, do_sample=False, | |
| pad_token_id=tok.pad_token_id) | |
| decoded = tok.decode(out[0, enc["input_ids"].shape[1]:], | |
| skip_special_tokens=True) | |
| parse = _extract_json(decoded) | |
| if parse is None: | |
| return "β ΧΧ ΧΧ¦ΧΧΧͺΧ ΧΧΧΧ₯ ΧΧͺ ΧΧ€Χ¨ΧΧΧ. Χ Χ‘Χ ΧΧ Χ‘Χ ΧΧΧΧ©." | |
| # 2. Apply defaults + complete | |
| final_parse = _apply_defaults(parse) | |
| try: | |
| completed = core.complete(DEMO_ISSUER, final_parse, _RNG) | |
| except Exception as e: | |
| return f"β Χ©ΧΧΧΧ ΧΧ’ΧΧΧΧ: {e}" | |
| # 3. Format | |
| return _format_document(completed) | |
| def start_bot(tok, model, discord_token: str = None): | |
| """ | |
| Start the Discord bot. Call this from Colab after loading tok + model. | |
| Args: | |
| tok: HuggingFace tokenizer (already loaded) | |
| model: fine-tuned model (already loaded, on GPU) | |
| discord_token: Bot token. Falls back to DISCORD_TOKEN env var. | |
| """ | |
| token = discord_token or os.environ.get("DISCORD_TOKEN", "") | |
| if not token: | |
| raise ValueError( | |
| "No Discord token found. " | |
| "Pass discord_token= or set os.environ['DISCORD_TOKEN']" | |
| ) | |
| intents = discord.Intents.default() | |
| intents.message_content = True | |
| bot = commands.Bot(command_prefix="!", intents=intents) | |
| async def on_ready(): | |
| print(f"β Discord bot ready: {bot.user} (id: {bot.user.id})") | |
| print(" Listening for: !receipt <Hebrew note>") | |
| async def receipt_cmd(ctx, *, note: str): | |
| """Convert a Hebrew income note to a fiscal document.""" | |
| await ctx.message.add_reaction("β³") | |
| try: | |
| # run blocking inference in executor to not block event loop | |
| loop = asyncio.get_event_loop() | |
| result = await loop.run_in_executor( | |
| None, run_pipeline, note, tok, model | |
| ) | |
| await ctx.reply(result) | |
| except Exception as e: | |
| await ctx.reply(f"β Χ©ΧΧΧΧ: {e}") | |
| finally: | |
| await ctx.message.remove_reaction("β³", bot.user) | |
| await ctx.message.add_reaction("β ") | |
| async def help_cmd(ctx): | |
| await ctx.reply( | |
| "**Text2Receipt Bot** π§Ύ\n" | |
| "ΧΧΧ¨ ΧΧ’Χ¨Χͺ ΧΧΧ Χ‘Χ Χ’ΧΧ¨ΧΧͺ ΧΧΧ‘ΧΧ Χ€ΧΧ‘Χ§ΧΧ ΧΧ©Χ¨ΧΧΧ.\n\n" | |
| "Χ©ΧΧΧΧ©: `!receipt <ΧΧ’Χ¨Χ ΧΧ’ΧΧ¨ΧΧͺ>`\n\n" | |
| "ΧΧΧΧΧΧΧͺ:\n" | |
| "β’ `!receipt Χ§ΧΧΧΧͺΧ 500 Χ©Χ§Χ ΧΧΧ©Χ Χ’Χ ΧΧΧ’ΧΧ₯`\n" | |
| "β’ `!receipt ΧΧ’ΧΧ ΧΧ’\"Χ Χ©ΧΧΧΧ 15,000 Χ©\"Χ Χ’Χ Χ€Χ¨ΧΧΧ§Χ ΧΧͺΧ¨`" | |
| ) | |
| # Run in background thread so Colab cell doesn't block | |
| def _run(): | |
| asyncio.run(bot.start(token)) | |
| t = threading.Thread(target=_run, daemon=True) | |
| t.start() | |
| print("π€ Bot thread started. Keep this cell running.") | |
| return bot | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Colab usage example (paste into a cell after loading tok + model in nb03): | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # import os | |
| # os.environ["DISCORD_TOKEN"] = "YOUR_BOT_TOKEN_HERE" | |
| # | |
| # from discord_bot import start_bot | |
| # bot = start_bot(tok, ft_model) # tok + ft_model from Β§5 of nb03 | |
| # | |
| # # Test locally without Discord: | |
| # from discord_bot import run_pipeline | |
| # print(run_pipeline("Χ§ΧΧΧΧͺΧ 1200 Χ©Χ§Χ ΧΧΧ©Χ ΧΧΧ Χ’Χ ΧΧΧ’ΧΧ₯ Χ’Χ‘Χ§Χ", tok, ft_model)) | |