File size: 28,661 Bytes
55e66ae
 
ee32286
cb39ff2
345b514
 
55e66ae
e3e9953
 
9e41a35
 
 
 
 
345b514
87de956
 
 
 
 
 
 
ad5f4e3
87de956
ad5f4e3
87de956
 
345b514
 
 
87de956
345b514
 
 
ad5f4e3
345b514
e3e9953
345b514
ad5f4e3
345b514
ad5f4e3
345b514
 
ee32286
345b514
 
 
 
874a7fc
ee32286
 
4111d7e
ad5f4e3
 
345b514
 
e3e9953
345b514
 
 
 
ad5f4e3
e3e9953
 
 
ad5f4e3
345b514
e3e9953
1479f95
 
 
 
 
ad5f4e3
1479f95
ad5f4e3
1479f95
fc878c5
345b514
 
 
 
 
 
 
ad5f4e3
345b514
ad5f4e3
e3e9953
ad5f4e3
ee32286
 
 
 
87814a3
 
 
ee32286
ad5f4e3
ee32286
 
 
 
 
 
 
 
87814a3
ee32286
 
 
 
 
 
5739656
73b265e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a82c09f
 
 
73b265e
a82c09f
 
 
73b265e
a82c09f
 
 
 
 
73b265e
a82c09f
 
ad5f4e3
a82c09f
 
 
73b265e
 
 
 
 
 
a82c09f
73b265e
 
a82c09f
 
9e41a35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eebd05c
9e41a35
 
 
 
 
 
 
 
 
 
 
 
eebd05c
 
9e41a35
fc878c5
9e41a35
fc878c5
 
 
 
 
 
 
 
 
 
9e41a35
5739656
e3e9953
019947c
e3e9953
9e41a35
 
 
345b514
fc878c5
345b514
 
 
fc878c5
345b514
e3e9953
874a7fc
83fccc2
ad5f4e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee32286
ad5f4e3
ee32286
ad5f4e3
fc878c5
a82c09f
ad5f4e3
fc878c5
83fccc2
 
fc878c5
ee32286
fc878c5
e3e9953
87814a3
fc878c5
87814a3
62260f2
87814a3
62260f2
 
87814a3
 
62260f2
25fc64e
 
87814a3
 
25fc64e
 
 
 
 
 
87814a3
 
25fc64e
 
 
 
 
 
 
87814a3
25fc64e
 
87814a3
 
25fc64e
 
 
87814a3
 
25fc64e
 
 
 
87814a3
f16bb04
87814a3
 
25fc64e
f16bb04
87814a3
 
 
25fc64e
b7808ff
 
f16bb04
87814a3
 
25fc64e
f16bb04
87814a3
 
62adec1
 
 
 
 
 
 
 
 
 
 
 
62260f2
 
 
25fc64e
 
 
62260f2
 
 
 
 
 
 
 
fc878c5
62260f2
 
 
 
 
 
 
 
4db59f9
25fc64e
87814a3
 
96cec36
25fc64e
 
 
b7808ff
25fc64e
96cec36
 
b7808ff
 
 
96cec36
b7808ff
f16bb04
2670bd2
 
3cd8c27
 
b7808ff
 
25fc64e
 
96cec36
 
 
 
 
 
25fc64e
 
 
b7808ff
87814a3
2670bd2
3cd8c27
2670bd2
 
 
 
b7808ff
2670bd2
3cd8c27
2670bd2
 
 
 
 
 
3cd8c27
 
 
 
 
87814a3
 
cb78d55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f16bb04
cb80818
25fc64e
 
87814a3
25fc64e
f16bb04
25fc64e
 
cb80818
87814a3
 
25fc64e
 
 
87814a3
 
 
 
 
f16bb04
87814a3
5739656
25fc64e
 
87814a3
 
f16bb04
019947c
25fc64e
87814a3
 
 
019947c
87814a3
25fc64e
87814a3
5739656
 
 
1c52644
 
 
5739656
9e41a35
1c52644
5739656
87814a3
25fc64e
87814a3
cb80818
25fc64e
5739656
25fc64e
 
019947c
25fc64e
62260f2
25fc64e
 
 
 
1c52644
25fc64e
62260f2
25fc64e
 
87814a3
25fc64e
019947c
25fc64e
62260f2
25fc64e
 
 
fc878c5
1c52644
fc878c5
 
 
 
 
b7808ff
f16bb04
2670bd2
f16bb04
2670bd2
b7808ff
2670bd2
019947c
f16bb04
62adec1
 
b7808ff
 
 
 
f16bb04
b7808ff
2670bd2
019947c
f16bb04
62adec1
 
 
b7808ff
 
 
62adec1
b7808ff
2670bd2
019947c
62adec1
 
 
 
b7808ff
 
 
f16bb04
25fc64e
87814a3
ad5f4e3
5739656
fc878c5
87814a3
 
b7808ff
25fc64e
62adec1
 
b7808ff
 
 
 
25fc64e
b7808ff
25fc64e
62adec1
 
 
b7808ff
 
 
62adec1
b7808ff
62adec1
 
 
 
b7808ff
 
 
87814a3
 
 
25fc64e
 
87814a3
 
e3e9953
 
fda10ed
1
2
3
4
5
6
7
8
9
10
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
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 &bull; 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)