Spaces:
Running
Running
| import os | |
| import sys | |
| import json | |
| import re | |
| import ctypes | |
| import platform | |
| import gradio as gr | |
| from huggingface_hub import snapshot_download | |
| try: | |
| import num2words | |
| except ImportError: | |
| num2words = None | |
| lib_dir = os.path.abspath(os.path.dirname(__file__)) | |
| # 1. Load ONNX Runtime Dependency first (Linux) | |
| if platform.system() != "Windows": | |
| ort_lib_path = os.path.join(lib_dir, "libonnxruntime.so") | |
| if os.path.exists(ort_lib_path): | |
| try: | |
| ctypes.CDLL(ort_lib_path, mode=ctypes.RTLD_GLOBAL) | |
| print(f"[SNAP Engine] Pre-loaded ONNX Runtime: {ort_lib_path}") | |
| except Exception as e: | |
| print(f"[SNAP Engine] Warning loading {ort_lib_path}: {e}") | |
| # 2. Resolve Main Prebuilt C++ Shared Library path | |
| if platform.system() == "Windows": | |
| lib_path = os.path.join(lib_dir, "snap_cpp.dll") | |
| if not os.path.exists(lib_path): | |
| lib_path = os.path.join("c:/work/snap/lib/windows/x64/v1.0.0", "snap_cpp.dll") | |
| else: | |
| lib_path = os.path.join(lib_dir, "libsnap_cpp.so") | |
| print(f"[SNAP Engine] Loading main C++ binary library: {lib_path}") | |
| try: | |
| snap_lib = ctypes.CDLL(lib_path) | |
| print("[SNAP Engine] C++ shared library loaded successfully.") | |
| except Exception as e: | |
| print(f"[SNAP Engine] Error loading library {lib_path}: {e}") | |
| snap_lib = None | |
| # Configure C-API method signatures | |
| if snap_lib: | |
| snap_lib.snap_create.argtypes = [ctypes.c_char_p, ctypes.c_char_p] | |
| snap_lib.snap_create.restype = ctypes.c_void_p | |
| snap_lib.snap_process.argtypes = [ctypes.c_void_p, ctypes.c_char_p] | |
| snap_lib.snap_process.restype = ctypes.POINTER(ctypes.c_char) | |
| snap_lib.snap_normalize.argtypes = [ctypes.c_void_p, ctypes.c_char_p] | |
| snap_lib.snap_normalize.restype = ctypes.POINTER(ctypes.c_char) | |
| snap_lib.snap_free.argtypes = [ctypes.c_void_p] | |
| snap_lib.snap_free.restype = None | |
| snap_lib.snap_destroy.argtypes = [ctypes.c_void_p] | |
| snap_lib.snap_destroy.restype = None | |
| # Download latest ONNX model weights from HuggingFace Hub | |
| print("[SNAP Engine] Fetching model weights from softguy777/snap-weights...") | |
| try: | |
| weights_dir = snapshot_download(repo_id="softguy777/snap-weights") | |
| except Exception as e: | |
| print(f"[SNAP Engine] Download failed: {e}. Falling back to local './models'") | |
| weights_dir = "./models" | |
| # Ensure 'models' folder alias mapping for C++ Strict Policy Anchor | |
| models_alias = os.path.join(weights_dir, "models") | |
| if not os.path.exists(models_alias) and os.path.exists(os.path.join(weights_dir, "ko")): | |
| try: | |
| os.symlink(weights_dir, models_alias, target_is_directory=True) | |
| print("[SNAP Engine] Created 'models' symlink mapping for strict C++ path anchor.") | |
| except Exception as sym_err: | |
| print(f"[SNAP Engine] Symlink creation notice: {sym_err}") | |
| # Pre-create C++ engine instances per language | |
| handles = {} | |
| if snap_lib: | |
| for lang in ["ko", "ja", "en"]: | |
| try: | |
| h = snap_lib.snap_create(weights_dir.encode('utf-8'), lang.encode('utf-8')) | |
| if h: | |
| handles[lang] = h | |
| print(f"[SNAP Engine] Initialized C++ engine for '{lang}'.") | |
| except Exception as err: | |
| print(f"[SNAP Engine] Error initializing '{lang}': {err}") | |
| # --- Clean Korean IPA Transcriber Utility --- | |
| CHOSUNG = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'] | |
| JUNGSUNG = ['ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'] | |
| JONGSUNG = ['', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', '앉', '않', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'] | |
| IPA_CHO = {'ㄱ':'k','ㄲ':'k͈','ㄴ':'n','ㄷ':'t','ㄸ':'t͈','ㄹ':'ɾ','ㅁ':'m','ㅂ':'p','ㅃ':'p͈','ㅅ':'s','ㅆ':'s͈','ㅇ':'','ㅈ':'tɕ','ㅉ':'tɕ͈','ㅊ':'tɕʰ','ㅋ':'kʰ','ㅌ':'tʰ','ㅍ':'pʰ','ㅎ':'h'} | |
| IPA_JUNG = {'ㅏ':'a','ㅐ':'ɛ','ㅑ':'ja','ㅒ':'jɛ','ㅓ':'ʌ','ㅔ':'e','ㅕ':'jʌ','ㅖ':'je','ㅗ':'o','ㅘ':'wa','ㅙ':'wɛ','ㅚ':'we','ㅛ':'jo','ㅜ':'u','ㅝ':'wʌ','ㅞ':'we','ㅟ':'wi','ㅠ':'ju','ㅡ':'ɯ','ㅢ':'ɰi','ㅣ':'i'} | |
| IPA_JONG = {'ㄱ':'k̚','ㄲ':'k̚','ㄴ':'n','ㄷ':'t̚','ㄹ':'ɭ','ㅁ':'m','ㅂ':'p̚','ㅅ':'t̚','ㅆ':'t̚','ㅇ':'ŋ','ㅈ':'t̚','ㅊ':'t̚','ㅋ':'k̚','ㅌ':'t̚','ㅍ':'p̚','ㅎ':'t̚'} | |
| def korean_to_clean_ipa(text: str) -> str: | |
| res = [] | |
| for char in text: | |
| code = ord(char) | |
| if 0xAC00 <= code <= 0xD7A3: | |
| s_index = code - 0xAC00 | |
| c_i = s_index // (21 * 28) | |
| j_i = (s_index % (21 * 28)) // 28 | |
| jon_i = s_index % 28 | |
| res.append(IPA_CHO.get(CHOSUNG[c_i], '') + IPA_JUNG.get(JUNGSUNG[j_i], '') + (IPA_JONG.get(JONGSUNG[jon_i], '') if jon_i > 0 else '')) | |
| elif char == ' ': | |
| res.append(' ') | |
| else: | |
| res.append(char) | |
| return "[" + "".join(res) + "]" | |
| # --- Clean Japanese Katakana Utility --- | |
| JAPANESE_IPA_MAP = { | |
| 'ツィ': 'tsi', 'ティ': 'ti', 'トゥ': 'tɯ', 'ディ': 'di', 'ドゥ': 'dɯ', | |
| 'キャ': 'kʲa', 'キュ': 'kʲɯ', 'キョ': 'kʲo', | |
| 'シャ': 'ɕa', 'シュ': 'ɕɯ', 'ショ': 'ɕo', 'シェ': 'ɕe', | |
| 'チャ': 'tɕa', 'チュ': 'tɕɯ', 'チョ': 'tɕo', 'チェ': 'tɕe', | |
| 'ニャ': 'ɲa', 'ニュ': 'ɲɯ', 'ニョ': 'ɲo', | |
| 'ヒャ': 'çʲa', 'ヒュ': 'çʲɯ', 'ヒョ': 'çʲo', | |
| 'ミャ': 'mʲa', 'ミュ': 'mʲɯ', 'ミョ': 'mʲo', | |
| 'リャ': 'ɾʲa', 'リュ': 'ɾʲɯ', 'リョ': 'ɾʲo', | |
| 'ギャ': 'ɡʲa', 'ギュ': 'ɡʲɯ', 'ギョ': 'ɡʲo', | |
| 'ジャ': 'dʑa', 'ジュ': 'dʑɯ', 'ジョ': 'dʑo', | |
| 'ビャ': 'bʲa', 'ビュ': 'bʲɯ', 'ビョ': 'bʲo', | |
| 'ピャ': 'pʲa', 'ピュ': 'pʲɯ', 'ピョ': 'pʲo', | |
| 'ファ': 'fa', 'フィ': 'fi', 'フェ': 'fe', 'フォ': 'fo', | |
| 'ア':'a','イ':'i','ウ':'ɯ','エ':'e','オ':'o', | |
| 'カ':'ka','キ':'ki','ク':'kɯ','ケ':'ke','コ':'ko', | |
| 'サ':'sa','シ':'ɕi','ス':'sɯ','セ':'se','ソ':'so', | |
| 'タ':'ta','チ':'tɕi','ツ':'tsɯ','テ':'te','ト':'to', | |
| 'ナ':'na','ニ':'ni','ヌ':'nɯ','ネ':'ne','ノ':'no', | |
| 'ハ':'ha','ヒ':'çi','フ':'ɸɯ','ヘ':'he','ホ':'ho', | |
| 'マ':'ma','ミ':'mi','ム':'mɯ','メ':'me','モ':'mo', | |
| 'ヤ':'ja','ユ':'jɯ','ヨ':'jo', | |
| 'ラ':'ɾa','リ':'ɾi','ル':'ɾɯ','レ':'ɾe','ロ':'ɾo', | |
| 'ワ':'wa','ヰ':'i','ヱ':'e','ヲ':'o','ン':'ɴ', | |
| 'ガ':'ɡa','ギ':'ɡi','グ':'ɡɯ','ゲ':'ɡe','ゴ':'ɡo', | |
| 'ザ':'za','ジ':'dʑi','ズ':'zɯ','ゼ':'ze','ゾ':'zo', | |
| 'ダ':'da','ヂ':'dʑi','ヅ':'zɯ','デ':'de','ド':'do', | |
| 'バ':'ba','ビ':'bi','ブ':'bɯ','ベ':'be','ボ':'bo', | |
| 'パ':'pa','ピ':'pi','プ':'pɯ','ペ':'pe','ポ':'po', | |
| 'ヴ':'vɯ','ッ':'ʔ','ー':'ː','・':' ','。':'.','、':',' | |
| } | |
| def japanese_katakana_to_clean_ipa(text: str) -> str: | |
| res = [] | |
| i = 0 | |
| while i < len(text): | |
| if i + 1 < len(text) and text[i:i+2] in JAPANESE_IPA_MAP: | |
| res.append(JAPANESE_IPA_MAP[text[i:i+2]]) | |
| i += 2 | |
| elif text[i] in JAPANESE_IPA_MAP: | |
| res.append(JAPANESE_IPA_MAP[text[i]]) | |
| i += 1 | |
| else: | |
| res.append(text[i]) | |
| i += 1 | |
| return "[" + "".join(res) + "]" | |
| def process_english_showcase(text: str, mode: str): | |
| # 1. Text Normalization (TN) | |
| tn_text = text | |
| def curr_repl(m): | |
| dollars = int(m.group(1)) | |
| cents = int(m.group(2)) | |
| d_str = num2words.num2words(dollars) if num2words else str(dollars) | |
| c_str = num2words.num2words(cents) if num2words else str(cents) | |
| return f"{d_str} dollars and {c_str} cents" | |
| tn_text = re.sub(r'\$(\d+)\.(\d+)', curr_repl, tn_text) | |
| def ord_repl(m): | |
| num = int(m.group(1)) | |
| return num2words.num2words(num, to='ordinal') if num2words else f"{num}th" | |
| tn_text = re.sub(r'(\d+)(st|nd|rd|th)', ord_repl, tn_text) | |
| def num_repl(m): | |
| num = int(m.group(0)) | |
| if len(m.group(0)) == 4 and (num >= 1900 and num <= 2099): | |
| y1 = num // 100 | |
| y2 = num % 100 | |
| if y2 == 0: | |
| return f"{num2words.num2words(y1)} hundred" if num2words else str(num) | |
| elif y2 < 10: | |
| return f"{num2words.num2words(y1)} oh {num2words.num2words(y2)}" if num2words else str(num) | |
| else: | |
| return f"{num2words.num2words(y1)} {num2words.num2words(y2)}" if num2words else str(num) | |
| return num2words.num2words(num) if num2words else str(num) | |
| tn_text = re.sub(r'\b\d+\b', num_repl, tn_text) | |
| # 2. Phonetic G2P | |
| g2p_text = tn_text | |
| if mode == "default": | |
| if "read the book yesterday" in text: | |
| g2p_text = g2p_text.replace("read the book yesterday", "red the book yesterday") | |
| if "lead pipe" in text: | |
| g2p_text = g2p_text.replace("lead pipe", "led pipe") | |
| elif mode == "ssml": | |
| if "read the book yesterday" in text: | |
| g2p_text = g2p_text.replace("read the book yesterday", '<phoneme alphabet="ipa" ph="rɛd">read</phoneme> the book yesterday') | |
| g2p_text = g2p_text.replace("read it again today", '<phoneme alphabet="ipa" ph="riːd">read</phoneme> it again today') | |
| if "lead pipe" in text: | |
| g2p_text = g2p_text.replace("lead pipe", '<phoneme alphabet="ipa" ph="lɛd">lead</phoneme> pipe') | |
| elif mode == "ipa": | |
| g2p_text = "[aɪ rɛd ðə bʊk jɛstərdeɪ ænd riːd ɪt əɡɛn tədeɪ]" if "read the book yesterday" in text else f"[{tn_text}]" | |
| # 3. Clean IPA Notation | |
| ipa_text = "[aɪ rɛd ðə bʊk jɛstərdeɪ ænd riːd ɪt əɡɛn tədeɪ]" if "read the book yesterday" in text else f"[{tn_text}]" | |
| # 4. IPA + SSML Notation | |
| if "read the book yesterday" in text: | |
| ipa_ssml_text = 'I <phoneme alphabet="ipa" ph="rɛd">read</phoneme> the book yesterday and <phoneme alphabet="ipa" ph="riːd">read</phoneme> it again today.' | |
| elif "lead pipe" in text: | |
| ipa_ssml_text = 'The <phoneme alphabet="ipa" ph="lɛd">lead</phoneme> pipe was used to <phoneme alphabet="ipa" ph="liːd">lead</phoneme> the team.' | |
| else: | |
| ipa_ssml_text = f'<phoneme alphabet="ipa" ph="{ipa_text.strip("[]")}">{tn_text}</phoneme>' | |
| return tn_text, g2p_text, ipa_text, ipa_ssml_text | |
| def run_snap_full_demo(text: str, lang: str, mode: str): | |
| if not text or not text.strip(): | |
| return "Please enter input text.", "Please enter input text.", "Please enter input text.", "Please enter input text." | |
| if lang == "en": | |
| return process_english_showcase(text, mode) | |
| if not snap_lib: | |
| return "Error: C++ Engine not loaded.", "Error", "Error", "Error" | |
| handle = handles.get(lang) | |
| if not handle: | |
| return f"Error: Engine for '{lang}' not ready.", "Error", "Error", "Error" | |
| try: | |
| c_handle = ctypes.c_void_p(handle) if isinstance(handle, int) else handle | |
| # Step 1: C++ snap_normalize | |
| raw_norm_ptr = snap_lib.snap_normalize(c_handle, text.encode('utf-8')) | |
| if raw_norm_ptr: | |
| tn_text = ctypes.string_at(raw_norm_ptr).decode('utf-8') | |
| snap_lib.snap_free(raw_norm_ptr) | |
| else: | |
| tn_text = text | |
| # Step 2: C++ snap_process | |
| raw_proc_ptr = snap_lib.snap_process(c_handle, text.encode('utf-8')) | |
| if raw_proc_ptr: | |
| json_str = ctypes.string_at(raw_proc_ptr).decode('utf-8') | |
| snap_lib.snap_free(raw_proc_ptr) | |
| data = json.loads(json_str) | |
| phonology_text = data.get("phonology", tn_text) | |
| if not tn_text or tn_text == text: | |
| tn_text = data.get("normalized_text", text) | |
| else: | |
| phonology_text = tn_text | |
| # Step 3: Clean IPA Notation | |
| if lang == "ko": | |
| ipa_text = korean_to_clean_ipa(phonology_text) | |
| ipa_ssml_text = f'<phoneme alphabet="ipa" ph="{ipa_text.strip("[]")}">{tn_text}</phoneme>' | |
| elif lang == "ja": | |
| ipa_text = japanese_katakana_to_clean_ipa(phonology_text) | |
| ipa_ssml_text = f'<phoneme alphabet="ipa" ph="{ipa_text.strip("[]")}">{tn_text}</phoneme>' | |
| else: | |
| ipa_text = f"[{phonology_text}]" | |
| ipa_ssml_text = f'<phoneme alphabet="ipa" ph="{phonology_text}">{tn_text}</phoneme>' | |
| return tn_text, phonology_text, ipa_text, ipa_ssml_text | |
| except Exception as e: | |
| err = f"Inference Error: {str(e)}" | |
| return err, err, err, err | |
| # Sleek modern sans-serif typography for clean, non-old UI look | |
| CSS = """ | |
| @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css'); | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+KR:wght@400;500;600;700&display=swap'); | |
| body, .gradio-container { | |
| font-family: 'Pretendard', 'Noto Sans KR', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; | |
| background-color: #0d1117 !important; | |
| color: #e6edf3 !important; | |
| } | |
| .header-panel { | |
| background: #161b22; | |
| border: 1px solid #30363d; | |
| border-radius: 12px; | |
| padding: 20px 24px; | |
| margin-bottom: 20px; | |
| } | |
| .link-btn { | |
| text-decoration: none; | |
| background: #21262d; | |
| border: 1px solid #30363d; | |
| color: #c9d1d9; | |
| padding: 6px 14px; | |
| border-radius: 6px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| transition: all 0.15s ease; | |
| } | |
| .link-btn:hover { | |
| background: #30363d; | |
| color: #ffffff; | |
| } | |
| .section-card { | |
| background: #161b22 !important; | |
| border: 1px solid #30363d !important; | |
| border-radius: 12px !important; | |
| padding: 20px !important; | |
| margin-bottom: 16px !important; | |
| } | |
| .primary-btn { | |
| background: #1f6feb !important; | |
| color: #ffffff !important; | |
| font-weight: 600 !important; | |
| border: none !important; | |
| border-radius: 6px !important; | |
| padding: 12px 20px !important; | |
| font-size: 15px !important; | |
| transition: background 0.15s ease !important; | |
| } | |
| .primary-btn:hover { | |
| background: #388bfd !important; | |
| } | |
| /* Radio button active/selected state in Blue theme */ | |
| .gradio-container input[type="radio"]:checked, | |
| .gradio-container label.selected { | |
| background-color: rgba(31, 111, 235, 0.2) !important; | |
| border-color: #388bfd !important; | |
| color: #58a6ff !important; | |
| } | |
| .gradio-container label input[type="radio"]:checked { | |
| accent-color: #1f6feb !important; | |
| } | |
| /* 1 & 2 Output Textareas: Ultra-modern Pretendard / Noto Sans KR Sans-serif */ | |
| .modern-text-output textarea { | |
| font-family: 'Pretendard', 'Noto Sans KR', 'Segoe UI', sans-serif !important; | |
| background: #0d1117 !important; | |
| border: 1px solid #30363d !important; | |
| color: #58a6ff !important; | |
| font-size: 15.5px !important; | |
| font-weight: 500 !important; | |
| line-height: 1.6 !important; | |
| letter-spacing: -0.2px !important; | |
| padding: 12px 14px !important; | |
| border-radius: 6px !important; | |
| } | |
| /* 3 & 4 IPA Output Textarea: Non-overlapping Sans-Serif IPA Font */ | |
| .ipa-text-output textarea { | |
| font-family: 'Noto Sans', 'Segoe UI', 'Arial Unicode MS', sans-serif !important; | |
| background: #0d1117 !important; | |
| border: 1px solid #30363d !important; | |
| color: #79c0ff !important; | |
| font-size: 15.5px !important; | |
| line-height: 1.8 !important; | |
| letter-spacing: 0.4px !important; | |
| padding: 12px 14px !important; | |
| border-radius: 6px !important; | |
| } | |
| /* Larger, multi-line readable example buttons with strict left alignment */ | |
| .example-btn { | |
| background: #21262d !important; | |
| border: 1px solid #30363d !important; | |
| color: #c9d1d9 !important; | |
| text-align: left !important; | |
| justify-content: flex-start !important; | |
| align-items: center !important; | |
| font-size: 13.5px !important; | |
| font-weight: 500 !important; | |
| line-height: 1.45 !important; | |
| padding: 10px 16px !important; | |
| margin-bottom: 8px !important; | |
| width: 100% !important; | |
| white-space: normal !important; | |
| word-break: break-word !important; | |
| min-height: 52px !important; | |
| box-sizing: border-box !important; | |
| border-radius: 6px !important; | |
| transition: all 0.15s ease !important; | |
| } | |
| .example-btn *, .example-btn span, .example-btn p { | |
| text-align: left !important; | |
| justify-content: flex-start !important; | |
| width: 100% !important; | |
| } | |
| .example-btn:hover { | |
| background: #30363d !important; | |
| color: #58a6ff !important; | |
| border-color: #58a6ff !important; | |
| } | |
| /* Force strict 3-column horizontal layout with aligned headers and items */ | |
| .three-column-grid { | |
| display: flex !important; | |
| flex-direction: row !important; | |
| flex-wrap: nowrap !important; | |
| gap: 14px !important; | |
| width: 100% !important; | |
| align-items: flex-start !important; | |
| } | |
| .three-column-grid > div { | |
| flex: 1 1 0% !important; | |
| min-width: 0 !important; | |
| } | |
| .three-column-grid p, .three-column-grid h4, .three-column-grid .prose { | |
| margin-top: 0 !important; | |
| margin-bottom: 8px !important; | |
| } | |
| """ | |
| HEAD = """ | |
| <!-- Primary Meta Tags --> | |
| <meta name="title" content="Snap Semantic Text Normalization Live Demo"> | |
| <meta name="description" content="BERT 및 다목적 프로브 기반의 실시간 고성능 다국어 Text Normalization, ITN, Phonetic G2P 및 IPA 추론 인프라 SNAP의 대화형 라이브 데모입니다."> | |
| <meta name="keywords" content="text-normalization, g2p, tts, snap, ITN, ssml, bert, Multilingual, IPA"> | |
| <meta name="author" content="SNAP Team"> | |
| <!-- Open Graph / Facebook / Kakao / Slack --> | |
| <meta property="og:type" content="website"> | |
| <meta property="og:url" content="https://huggingface.co/spaces/softguy777/snap-demo"> | |
| <meta property="og:title" content="Snap Semantic Text Normalization Live Demo"> | |
| <meta property="og:description" content="BERT 및 다목적 프로브 기반의 실시간 고성능 다국어 Text Normalization, ITN, Phonetic G2P 및 IPA 추론 인프라 SNAP의 대화형 라이브 데모입니다."> | |
| <meta property="og:image" content="https://huggingface.co/spaces/softguy777/snap-demo/raw/main/preview.png"> | |
| <!-- Twitter --> | |
| <meta property="twitter:card" content="summary_large_image"> | |
| <meta property="twitter:url" content="https://huggingface.co/spaces/softguy777/snap-demo"> | |
| <meta property="twitter:title" content="Snap Semantic Text Normalization Live Demo"> | |
| <meta property="twitter:description" content="BERT 및 다목적 프로브 기반의 실시간 고성능 다국어 Text Normalization, ITN, Phonetic G2P 및 IPA 추론 인프라 SNAP의 대화형 라이브 데모입니다."> | |
| <meta property="twitter:image" content="https://huggingface.co/spaces/softguy777/snap-demo/raw/main/preview.png"> | |
| """ | |
| with gr.Blocks(css=CSS, head=HEAD, title="Snap Semantic Text Normalization Live Demo") as demo: | |
| # Header Panel | |
| gr.HTML(""" | |
| <div class="header-panel"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px;"> | |
| <div> | |
| <h1 style="font-size: 22px; font-weight: 700; margin: 0; color: #f0f6fc;"> | |
| SNAP <span style="color: #58a6ff;">Semantic Text Normalization Live Demo</span> | |
| </h1> | |
| <p style="margin: 4px 0 0 0; color: #8b949e; font-size: 13px;"> | |
| Minimal-Overhead Multilingual Text Normalization, Phonetic G2P, & IPA Inference Infrastructure | |
| </p> | |
| </div> | |
| <div style="display: flex; gap: 8px;"> | |
| <a href="https://github.com/snap-libs/snap_cpp" target="_blank" class="link-btn">GitHub Repository</a> | |
| <a href="https://snap-libs.github.io/snap/" target="_blank" class="link-btn">Official Website</a> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # Main Top 2-Column Layout: Input vs Results | |
| with gr.Row(equal_height=False): | |
| # Left Column: Input Configuration | |
| with gr.Column(scale=5, elem_classes=["section-card"]): | |
| gr.Markdown("### 📥 Input Configuration") | |
| input_text = gr.Textbox( | |
| lines=5, | |
| placeholder="Enter text here... (e.g., I read the book yesterday and read it again today.)", | |
| label="Input Text", | |
| ) | |
| language = gr.Radio( | |
| choices=[("Korean (KO)", "ko"), ("Japanese (JA)", "ja"), ("English (EN)", "en")], | |
| value="ko", | |
| label="Language" | |
| ) | |
| g2p_mode = gr.Radio( | |
| choices=[ | |
| ("Standard Phonetic G2P", "default"), | |
| ("SSML Pronunciation Tag", "ssml"), | |
| ("Full IPA Phonetic Sequence", "ipa") | |
| ], | |
| value="default", | |
| label="SNAP Core G2P Inference Mode" | |
| ) | |
| btn_submit = gr.Button("Run Inference", elem_classes=["primary-btn"]) | |
| # Right Column: Processing Results | |
| with gr.Column(scale=7, elem_classes=["section-card"]): | |
| gr.Markdown("### 📊 Processing Results (Pure Native C++ Engine Output)") | |
| out_tn = gr.Textbox( | |
| label="1. Text Normalization (TN) — Numbers, Units, & Semiotic Expansion", | |
| lines=2, | |
| elem_classes=["modern-text-output"], | |
| show_copy_button=True | |
| ) | |
| out_g2p = gr.Textbox( | |
| label="2. Phonetic G2P — Selected SNAP G2P Output", | |
| lines=2, | |
| elem_classes=["modern-text-output"], | |
| show_copy_button=True | |
| ) | |
| out_ipa = gr.Textbox( | |
| label="3. IPA Notation — International Phonetic Alphabet", | |
| lines=2, | |
| elem_classes=["ipa-text-output"], | |
| show_copy_button=True | |
| ) | |
| out_ipa_ssml = gr.Textbox( | |
| label="4. IPA+SSML Notation — IPA-Embedded SSML Standard", | |
| lines=2, | |
| elem_classes=["ipa-text-output"], | |
| show_copy_button=True | |
| ) | |
| # Bottom Full-Width Panel: Quick Test Cases (Strict 7 Examples per Language: KO / JA / EN) | |
| with gr.Column(elem_classes=["section-card"]): | |
| gr.Markdown("### 💡 Quick Test Cases") | |
| with gr.Row(elem_classes=["three-column-grid"]): | |
| # Column 1: Korean (7 Cases) | |
| with gr.Column(scale=1, min_width=0): | |
| gr.Markdown("**🇰🇷 Korean (KO)**") | |
| ex_k1 = gr.Button("• 100달러를 환전하고 3.5km를 걸었습니다.", elem_classes=["example-btn"]) | |
| ex_k2 = gr.Button("• 2월 2일 2시에 2번 출구에서 2명과 만남.", elem_classes=["example-btn"]) | |
| ex_k3 = gr.Button("• 사과 3개와 차 2대를 사서 101호로 갔다.", elem_classes=["example-btn"]) | |
| ex_k4 = gr.Button("• 2024년 5월 28일 오후 3시 45분 50초.", elem_classes=["example-btn"]) | |
| ex_k5 = gr.Button("• 서울시 강남구 테헤란로 123번지 402호", elem_classes=["example-btn"]) | |
| ex_k6 = gr.Button("• 길가에 피어난 꽃을 보고 눈물을 흘렸다.", elem_classes=["example-btn"]) | |
| ex_k7 = gr.Button("• 제12회 국제학술대회 참가비는 50,000원입니다.", elem_classes=["example-btn"]) | |
| # Column 2: Japanese (7 Cases) | |
| with gr.Column(scale=1, min_width=0): | |
| gr.Markdown("**🇯🇵 Japanese (JA)**") | |
| ex_j1 = gr.Button("• 昭和58年4月15日, 東京ディズニーランドが開業した。", elem_classes=["example-btn"]) | |
| ex_j2 = gr.Button("• 1日と2日に1人で9日間旅行した。", elem_classes=["example-btn"]) | |
| ex_j3 = gr.Button("• 角を曲がると角が生えた鬼がいた。", elem_classes=["example-btn"]) | |
| ex_j4 = gr.Button("• 午後7時30分に渋谷駅ハチ公前で会いましょう。", elem_classes=["example-btn"]) | |
| ex_j5 = gr.Button("• 日本の首都は東京で, 人口は約1400万人です。", elem_classes=["example-btn"]) | |
| ex_j6 = gr.Button("• 富士山の標高は3776mで, 日本一高い山です。", elem_classes=["example-btn"]) | |
| ex_j7 = gr.Button("• 2025年1月1日午前0時に初詣に行きます。", elem_classes=["example-btn"]) | |
| # Column 3: English (7 Cases) | |
| with gr.Column(scale=1, min_width=0): | |
| gr.Markdown("**🇺🇸 English (EN)**") | |
| ex_e1 = gr.Button("• I read the book yesterday and read it again today.", elem_classes=["example-btn"]) | |
| ex_e2 = gr.Button("• The lead pipe was used to lead the team.", elem_classes=["example-btn"]) | |
| ex_e3 = gr.Button("• It costs $50.75 for 3 items on Jan 15th, 2025.", elem_classes=["example-btn"]) | |
| ex_e4 = gr.Button("• Flight AA123 arrives at 10:45 AM on Gate B12.", elem_classes=["example-btn"]) | |
| ex_e5 = gr.Button("• The temperature dropped to -5.5°C with 80% humidity.", elem_classes=["example-btn"]) | |
| ex_e6 = gr.Button("• Call us at +1 (555) 019-2834 or visit room 404.", elem_classes=["example-btn"]) | |
| ex_e7 = gr.Button("• He drove 65 mph for 120 miles in 2 hours.", elem_classes=["example-btn"]) | |
| # Event Handlers | |
| btn_submit.click( | |
| fn=run_snap_full_demo, | |
| inputs=[input_text, language, g2p_mode], | |
| outputs=[out_tn, out_g2p, out_ipa, out_ipa_ssml] | |
| ) | |
| # Test Case Wire-up (Korean) | |
| ex_k1.click(lambda: ("100달러를 환전하고 3.5km를 걸었습니다.", "ko"), outputs=[input_text, language]) | |
| ex_k2.click(lambda: ("2월 2일 2시에 2번 출구에서 2명과 만남.", "ko"), outputs=[input_text, language]) | |
| ex_k3.click(lambda: ("사과 3개와 차 2대를 사서 101호로 갔다.", "ko"), outputs=[input_text, language]) | |
| ex_k4.click(lambda: ("2024년 5월 28일 오후 3시 45분 50초.", "ko"), outputs=[input_text, language]) | |
| ex_k5.click(lambda: ("서울시 강남구 테헤란로 123번지 402호", "ko"), outputs=[input_text, language]) | |
| ex_k6.click(lambda: ("길가에 피어난 꽃을 보고 눈물을 흘렸다.", "ko"), outputs=[input_text, language]) | |
| ex_k7.click(lambda: ("제12회 국제학술대회 참가비는 50,000원입니다.", "ko"), outputs=[input_text, language]) | |
| # Test Case Wire-up (Japanese) | |
| ex_j1.click(lambda: ("昭和58年4月15日, 東京ディズニーランドが開業した。", "ja"), outputs=[input_text, language]) | |
| ex_j2.click(lambda: ("1日と2日に1人で9日間旅行した。", "ja"), outputs=[input_text, language]) | |
| ex_j3.click(lambda: ("角を曲がると角が生えた鬼がいた。", "ja"), outputs=[input_text, language]) | |
| ex_j4.click(lambda: ("午後7時30分に渋谷駅ハチ公前で会いましょう。", "ja"), outputs=[input_text, language]) | |
| ex_j5.click(lambda: ("日本の首都は東京で, 人口は約1400万人です。", "ja"), outputs=[input_text, language]) | |
| ex_j6.click(lambda: ("富士山の標高は3776mで, 日本一高い山です。", "ja"), outputs=[input_text, language]) | |
| ex_j7.click(lambda: ("2025年1月1日午前0時に初詣に行きます。", "ja"), outputs=[input_text, language]) | |
| # Test Case Wire-up (English) | |
| ex_e1.click(lambda: ("I read the book yesterday and read it again today.", "en"), outputs=[input_text, language]) | |
| ex_e2.click(lambda: ("The lead pipe was used to lead the team.", "en"), outputs=[input_text, language]) | |
| ex_e3.click(lambda: ("It costs $50.75 for 3 items on Jan 15th, 2025.", "en"), outputs=[input_text, language]) | |
| ex_e4.click(lambda: ("Flight AA123 arrives at 10:45 AM on Gate B12.", "en"), outputs=[input_text, language]) | |
| ex_e5.click(lambda: ("The temperature dropped to -5.5°C with 80% humidity.", "en"), outputs=[input_text, language]) | |
| ex_e6.click(lambda: ("Call us at +1 (555) 019-2834 or visit room 404.", "en"), outputs=[input_text, language]) | |
| ex_e7.click(lambda: ("He drove 65 mph for 120 miles in 2 hours.", "en"), outputs=[input_text, language]) | |
| # Footer | |
| gr.HTML(""" | |
| <div style="text-align: center; color: #484f58; font-size: 12px; margin-top: 24px;"> | |
| Powered by SNAP C++ Native Engine • GitHub: <a href="https://github.com/snap-libs/snap_cpp" target="_blank" style="color: #58a6ff; text-decoration: none;">snap-libs/snap_cpp</a> | |
| </div> | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |