Spaces:
Sleeping
Sleeping
Download app.py from frznkshn/TextNormalizer: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/frznkshn/TextNormalizer/resolve/main/app.py
- Command line
-
hf download hf://spaces/frznkshn/TextNormalizer/app.py
-
curl -L -o app.py https://huggingface.co/spaces/frznkshn/TextNormalizer/resolve/main/app.py
11.4 kB
| """Gradio UI for the text Preprocessor / Normalizer (big-screen / demo layout). | |
| Run: python app.py -> http://127.0.0.1:7860 | |
| python app.py --host 0.0.0.0 -> reachable from other computers | |
| Place this file next to normalizer.py (inside the Preprocessor folder). | |
| """ | |
| import argparse | |
| import ast | |
| import html | |
| import socket | |
| from pathlib import Path | |
| import gradio as gr | |
| from normalizer import Normalizer | |
| normalizer = Normalizer() | |
| HERE = Path(__file__).parent | |
| def normalize(text: str) -> str: | |
| if not text or not text.strip(): | |
| return "" | |
| # Process line by line so multi-line input keeps its structure. | |
| return "\n".join(normalizer.process(line) if line.strip() else line | |
| for line in text.splitlines()) | |
| def load_example_groups(path: Path = HERE / "simple_test.py") -> dict[str, list[list[str]]]: | |
| """Collect every `n.process("...")` input from simple_test.py, grouped by the | |
| test function it lives in (currency, units, ...). Stays in sync with the tests.""" | |
| if not path.exists(): | |
| return {} | |
| tree = ast.parse(path.read_text(encoding="utf-8")) | |
| groups, seen = {}, set() | |
| for func in tree.body: | |
| if not (isinstance(func, ast.FunctionDef) and func.name.startswith("test_")): | |
| continue | |
| title = (func.name.removeprefix("test_").removesuffix("_normalization") | |
| .replace("_", " ").strip().title()) | |
| title = {"Postal Nationalid": "Postal Code & National ID"}.get(title, title) | |
| items = [] | |
| for node in ast.walk(func): | |
| if (isinstance(node, ast.Call) | |
| and isinstance(node.func, ast.Attribute) | |
| and node.func.attr == "process" | |
| and node.args | |
| and isinstance(node.args[0], ast.Constant) | |
| and isinstance(node.args[0].value, str) | |
| and node.args[0].value not in seen): | |
| seen.add(node.args[0].value) | |
| items.append((node.lineno, node.args[0].value)) | |
| if items: | |
| groups[title] = [[s] for _, s in sorted(items)] | |
| return groups | |
| # ---- example gallery (custom HTML so we control the design) ----------------- | |
| PALETTE = ["#6366f1", "#0ea5e9", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#14b8a6"] | |
| def build_gallery(groups: dict[str, list[list[str]]]) -> str: | |
| colors = {name: PALETTE[i % len(PALETTE)] for i, name in enumerate(groups)} | |
| total = sum(len(v) for v in groups.values()) | |
| chips = [f'<button class="ex-chip active" data-cat="all">All <b>{total}</b></button>'] | |
| for name, rows in groups.items(): | |
| chips.append(f'<button class="ex-chip" data-cat="{html.escape(name)}" ' | |
| f'style="--c:{colors[name]}">{html.escape(name)} <b>{len(rows)}</b></button>') | |
| cards = [] | |
| for name, rows in groups.items(): | |
| for (text,) in rows: | |
| result = normalize(text) | |
| cards.append( | |
| f'<div class="ex-card" data-cat="{html.escape(name)}" style="--c:{colors[name]}" ' | |
| f'data-input="{html.escape(text, quote=True)}" ' | |
| f'data-search="{html.escape((text + " " + result).lower(), quote=True)}">' | |
| f'<span class="ex-tag">{html.escape(name)}</span>' | |
| f'<div class="ex-in" dir="auto">{html.escape(text)}</div>' | |
| f'<div class="ex-out" dir="auto">{html.escape(result)}</div>' | |
| f'</div>') | |
| return ( | |
| '<div id="gallery">' | |
| '<div class="ex-toolbar">' | |
| '<input id="ex-search" type="search" placeholder="Search examples… (جستجو)" autocomplete="off">' | |
| f'<div class="ex-chips">{"".join(chips)}</div>' | |
| f'<div id="ex-count">{total} examples</div>' | |
| '</div>' | |
| f'<div class="ex-grid">{"".join(cards)}</div>' | |
| '<div id="ex-empty" style="display:none">No examples match.</div>' | |
| '</div>') | |
| # Browser-side behaviour: click-to-fill, category filter, search, bidi alignment. | |
| HEAD = """ | |
| <script> | |
| const fixDir = () => document.querySelectorAll('textarea').forEach(t => t.setAttribute('dir', 'auto')); | |
| new MutationObserver(fixDir).observe(document.documentElement, {childList: true, subtree: true}); | |
| let activeCat = 'all'; | |
| function applyFilter() { | |
| const q = (document.getElementById('ex-search')?.value || '').trim().toLowerCase(); | |
| let shown = 0; | |
| document.querySelectorAll('#gallery .ex-card').forEach(c => { | |
| const ok = (activeCat === 'all' || c.dataset.cat === activeCat) && (!q || c.dataset.search.includes(q)); | |
| c.style.display = ok ? '' : 'none'; | |
| if (ok) shown++; | |
| }); | |
| const n = document.getElementById('ex-count'); if (n) n.textContent = shown + ' examples'; | |
| const e = document.getElementById('ex-empty'); if (e) e.style.display = shown ? 'none' : ''; | |
| } | |
| document.addEventListener('input', e => { if (e.target.id === 'ex-search') applyFilter(); }); | |
| document.addEventListener('click', e => { | |
| const chip = e.target.closest('.ex-chip'); | |
| if (chip) { | |
| activeCat = chip.dataset.cat; | |
| document.querySelectorAll('.ex-chip').forEach(c => c.classList.toggle('active', c === chip)); | |
| applyFilter(); return; | |
| } | |
| const card = e.target.closest('.ex-card'); | |
| if (card) { | |
| const ta = document.querySelector('#inp-box textarea'); | |
| if (!ta) return; | |
| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(ta, card.dataset.input); | |
| ta.dispatchEvent(new Event('input', {bubbles: true})); | |
| document.querySelectorAll('.ex-card.sel').forEach(c => c.classList.remove('sel')); | |
| card.classList.add('sel'); | |
| } | |
| }); | |
| </script> | |
| """ | |
| CSS = """ | |
| .gradio-container { max-width: 100% !important; font-size: 22px !important; | |
| font-family: Vazirmatn, "Segoe UI", Tahoma, Arial, sans-serif !important; } | |
| #title h1 { font-size: 2.6rem; margin: 0.2em 0; } | |
| #title p { font-size: 1.3rem; } | |
| /* converter stays on screen while the examples scroll */ | |
| #converter { position: sticky; top: 12px; align-self: flex-start; } | |
| #converter textarea { font-size: 1.9rem !important; line-height: 1.8 !important; } | |
| #converter label span, #converter .label-wrap span { font-size: 1.3rem !important; font-weight: 600; } | |
| #convert-btn { font-size: 1.6rem !important; padding: 0.7em 0 !important; } | |
| #clear-btn { font-size: 1.3rem !important; } | |
| /* example gallery */ | |
| /* Gradio's block wrapper forces overflow:visible, so the scrolling lives on our own inner element */ | |
| #gallery { max-height: 88vh; overflow-y: auto; overflow-x: hidden; padding: 0 10px 12px 0; | |
| overscroll-behavior: contain; scrollbar-width: thin; } | |
| #gallery::-webkit-scrollbar { width: 12px; } | |
| #gallery::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 8px; border: 3px solid transparent; background-clip: content-box; } | |
| .ex-toolbar { position: sticky; top: 0; z-index: 5; padding: 8px 0 12px; | |
| background: var(--background-fill-secondary, #f8fafc); } | |
| #ex-search { width: 100%; box-sizing: border-box; font-size: 1.4rem; padding: 0.55em 0.9em; | |
| border: 2px solid var(--border-color-primary, #cbd5e1); border-radius: 12px; | |
| background: var(--background-fill-primary, #fff); color: inherit; } | |
| #ex-search:focus { outline: none; border-color: #6366f1; } | |
| .ex-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 6px; } | |
| .ex-chip { --c: #475569; font-size: 1.15rem; padding: 0.35em 0.9em; border-radius: 999px; cursor: pointer; | |
| border: 2px solid var(--c); background: transparent; color: var(--c); font-family: inherit; } | |
| .ex-chip b { color: inherit !important; opacity: .8; font-weight: 600; margin-inline-start: 4px; } | |
| .ex-chip.active { background: var(--c); color: #fff; } | |
| #ex-count { font-size: 1.05rem; opacity: .65; } | |
| .ex-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 9px; } | |
| .ex-card { position: relative; cursor: pointer; padding: 8px 11px 8px; border-radius: 10px; | |
| background: var(--background-fill-primary, #fff); border: 1px solid var(--border-color-primary, #e2e8f0); | |
| border-inline-start: 5px solid var(--c); box-shadow: 0 1px 3px rgba(0,0,0,.06); | |
| transition: transform .12s, box-shadow .12s; } | |
| .ex-card:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0,0,0,.14); } | |
| .ex-card.sel { outline: 3px solid var(--c); } | |
| .ex-tag { position: absolute; top: 4px; inset-inline-end: 8px; font-size: .62rem; font-weight: 700; | |
| letter-spacing: .03em; color: var(--c); text-transform: uppercase; opacity: .85; } | |
| .ex-in { font-size: 1.1rem; line-height: 1.45; margin-top: 12px; word-break: break-word; } | |
| .ex-out { font-size: .88rem; line-height: 1.45; margin-top: 5px; padding-top: 5px; opacity: .72; | |
| border-top: 1px dashed var(--border-color-primary, #cbd5e1); word-break: break-word; } | |
| .ex-out::before { content: "→ "; color: var(--c); font-weight: 700; } | |
| #ex-empty { text-align: center; font-size: 1.3rem; opacity: .6; padding: 40px 0; } | |
| """ | |
| THEME = gr.themes.Soft(text_size="lg") | |
| with gr.Blocks(title="Text Normalizer") as demo: | |
| gr.Markdown("# Text Normalizer\n" | |
| "Type or paste text, or click any example card. " | |
| "The spoken-form text appears instantly.", elem_id="title") | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=4, elem_id="converter"): | |
| inp = gr.Textbox(label="Input", lines=5, max_lines=10, elem_id="inp-box", | |
| placeholder="شماره کارت من رو یادداشت کنین: 6200861916356305") | |
| out = gr.Textbox(label="Output", lines=5, max_lines=10, | |
| interactive=False, buttons=["copy"]) | |
| run = gr.Button("Convert", variant="primary", elem_id="convert-btn") | |
| gr.ClearButton([inp, out], elem_id="clear-btn") | |
| with gr.Column(scale=6): | |
| gr.HTML(build_gallery(load_example_groups()), elem_id="examples") | |
| run.click(normalize, inp, out) | |
| inp.change(normalize, inp, out, show_progress="hidden") # live conversion | |
| def lan_ip() -> str: | |
| try: | |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: | |
| sock.connect(("10.255.255.255", 1)) # no packet is sent | |
| return sock.getsockname()[0] | |
| except OSError: | |
| return "127.0.0.1" | |
| # if __name__ == "__main__": | |
| # parser = argparse.ArgumentParser() | |
| # parser.add_argument("--host", default="127.0.0.1", | |
| # help="use 0.0.0.0 to let other computers on the network connect") | |
| # parser.add_argument("--port", type=int, default=7860) | |
| # args = parser.parse_args() | |
| # if args.host == "0.0.0.0": | |
| # print(f"\n>>> Others on the same network can open: http://{lan_ip()}:{args.port}\n") | |
| # demo.launch(server_name=args.host, server_port=args.port, | |
| # head=HEAD, css=CSS, theme=THEME) | |
| import spaces | |
| def _gpu_placeholder(): | |
| pass | |
| import os | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| on_space = "SPACE_ID" in os.environ | |
| parser.add_argument("--host", default="0.0.0.0" if on_space else "127.0.0.1") | |
| parser.add_argument("--port", type=int, default=7860) | |
| args = parser.parse_args() | |
| if args.host == "0.0.0.0" and not on_space: | |
| print(f"\n>>> Others on the same network can open: http://{lan_ip()}:{args.port}\n") | |
| demo.launch(server_name=args.host, server_port=args.port, | |
| head=HEAD, css=CSS, theme=THEME, ssr_mode=False) |