thewh1teagle commited on
Commit
5d349c2
·
0 Parent(s):
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.onnx filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Text to speech in Hebrew
3
+ emoji: 🐢
4
+ colorFrom: red
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: "4.44.0"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
app.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ uv sync
3
+ uv pip install Flask
4
+ wget https://huggingface.co/thewh1teagle/phonikud-onnx/resolve/main/phonikud-1.0.int8.onnx
5
+ wget https://huggingface.co/thewh1teagle/phonikud-tts-checkpoints/resolve/main/model.onnx
6
+ wget https://huggingface.co/thewh1teagle/phonikud-tts-checkpoints/resolve/main/model.config.json
7
+ uv run ./examples/space_v1/app.py
8
+ """
9
+
10
+ from flask import Flask, render_template, request, jsonify
11
+ from phonikud_tts import Phonikud, phonemize, Piper
12
+ import soundfile as sf
13
+ import base64
14
+ import io
15
+
16
+ app = Flask(__name__)
17
+ phonikud = Phonikud("phonikud-1.0.int8.onnx")
18
+ piper = Piper("model.onnx", "model.config.json")
19
+
20
+ @app.route("/")
21
+ def index():
22
+ return render_template("index.html")
23
+
24
+ @app.route("/generate", methods=["POST"])
25
+ def generate():
26
+ mode = request.form["mode"]
27
+ text = request.form.get("text", "")
28
+ phonemes = request.form.get("phonemes", "")
29
+
30
+ if mode == "text":
31
+ with_diacritics = phonikud.add_diacritics(text)
32
+ phonemes = phonemize(with_diacritics)
33
+ elif mode == "diacritics":
34
+ with_diacritics = text
35
+ phonemes = phonemize(with_diacritics)
36
+ else:
37
+ with_diacritics = None
38
+
39
+ samples, sample_rate = piper.create(phonemes, is_phonemes=True, length_scale=1.25)
40
+ buffer = io.BytesIO()
41
+ sf.write(buffer, samples, sample_rate, format="WAV")
42
+ buffer.seek(0)
43
+ b64_audio = base64.b64encode(buffer.read()).decode("utf-8")
44
+ data_uri = f"data:audio/wav;base64,{b64_audio}"
45
+
46
+ return jsonify({
47
+ "diacritics": with_diacritics,
48
+ "phonemes": phonemes,
49
+ "audio": data_uri
50
+ })
51
+
52
+ @app.route("/audio/<filename>")
53
+ def serve_audio(filename):
54
+ return app.send_static_file(filename)
55
+
56
+ if __name__ == "__main__":
57
+ app.run(debug=True, host="0.0.0.0", port=7860)
model.config.json ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "",
3
+ "audio": {
4
+ "sample_rate": 22050,
5
+ "quality": "train"
6
+ },
7
+ "espeak": {
8
+ "voice": "he"
9
+ },
10
+ "language": {
11
+ "code": "he"
12
+ },
13
+ "inference": {
14
+ "noise_scale": 0.667,
15
+ "length_scale": 1,
16
+ "noise_w": 0.8
17
+ },
18
+ "phoneme_type": "raw",
19
+ "phoneme_map": {},
20
+ "phoneme_id_map": {
21
+ " ": [
22
+ 3
23
+ ],
24
+ "!": [
25
+ 4
26
+ ],
27
+ "\"": [
28
+ 150
29
+ ],
30
+ "#": [
31
+ 149
32
+ ],
33
+ "$": [
34
+ 2
35
+ ],
36
+ "'": [
37
+ 5
38
+ ],
39
+ "(": [
40
+ 6
41
+ ],
42
+ ")": [
43
+ 7
44
+ ],
45
+ ",": [
46
+ 8
47
+ ],
48
+ "-": [
49
+ 9
50
+ ],
51
+ ".": [
52
+ 10
53
+ ],
54
+ "0": [
55
+ 130
56
+ ],
57
+ "1": [
58
+ 131
59
+ ],
60
+ "2": [
61
+ 132
62
+ ],
63
+ "3": [
64
+ 133
65
+ ],
66
+ "4": [
67
+ 134
68
+ ],
69
+ "5": [
70
+ 135
71
+ ],
72
+ "6": [
73
+ 136
74
+ ],
75
+ "7": [
76
+ 137
77
+ ],
78
+ "8": [
79
+ 138
80
+ ],
81
+ "9": [
82
+ 139
83
+ ],
84
+ ":": [
85
+ 11
86
+ ],
87
+ ";": [
88
+ 12
89
+ ],
90
+ "?": [
91
+ 13
92
+ ],
93
+ "X": [
94
+ 156
95
+ ],
96
+ "^": [
97
+ 1
98
+ ],
99
+ "_": [
100
+ 0
101
+ ],
102
+ "a": [
103
+ 14
104
+ ],
105
+ "b": [
106
+ 15
107
+ ],
108
+ "c": [
109
+ 16
110
+ ],
111
+ "d": [
112
+ 17
113
+ ],
114
+ "e": [
115
+ 18
116
+ ],
117
+ "f": [
118
+ 19
119
+ ],
120
+ "g": [
121
+ 154
122
+ ],
123
+ "h": [
124
+ 20
125
+ ],
126
+ "i": [
127
+ 21
128
+ ],
129
+ "j": [
130
+ 22
131
+ ],
132
+ "k": [
133
+ 23
134
+ ],
135
+ "l": [
136
+ 24
137
+ ],
138
+ "m": [
139
+ 25
140
+ ],
141
+ "n": [
142
+ 26
143
+ ],
144
+ "o": [
145
+ 27
146
+ ],
147
+ "p": [
148
+ 28
149
+ ],
150
+ "q": [
151
+ 29
152
+ ],
153
+ "r": [
154
+ 30
155
+ ],
156
+ "s": [
157
+ 31
158
+ ],
159
+ "t": [
160
+ 32
161
+ ],
162
+ "u": [
163
+ 33
164
+ ],
165
+ "v": [
166
+ 34
167
+ ],
168
+ "w": [
169
+ 35
170
+ ],
171
+ "x": [
172
+ 36
173
+ ],
174
+ "y": [
175
+ 37
176
+ ],
177
+ "z": [
178
+ 38
179
+ ],
180
+ "æ": [
181
+ 39
182
+ ],
183
+ "ç": [
184
+ 40
185
+ ],
186
+ "ð": [
187
+ 41
188
+ ],
189
+ "ø": [
190
+ 42
191
+ ],
192
+ "ħ": [
193
+ 43
194
+ ],
195
+ "ŋ": [
196
+ 44
197
+ ],
198
+ "œ": [
199
+ 45
200
+ ],
201
+ "ǀ": [
202
+ 46
203
+ ],
204
+ "ǁ": [
205
+ 47
206
+ ],
207
+ "ǂ": [
208
+ 48
209
+ ],
210
+ "ǃ": [
211
+ 49
212
+ ],
213
+ "ɐ": [
214
+ 50
215
+ ],
216
+ "ɑ": [
217
+ 51
218
+ ],
219
+ "ɒ": [
220
+ 52
221
+ ],
222
+ "ɓ": [
223
+ 53
224
+ ],
225
+ "ɔ": [
226
+ 54
227
+ ],
228
+ "ɕ": [
229
+ 55
230
+ ],
231
+ "ɖ": [
232
+ 56
233
+ ],
234
+ "ɗ": [
235
+ 57
236
+ ],
237
+ "ɘ": [
238
+ 58
239
+ ],
240
+ "ə": [
241
+ 59
242
+ ],
243
+ "ɚ": [
244
+ 60
245
+ ],
246
+ "ɛ": [
247
+ 61
248
+ ],
249
+ "ɜ": [
250
+ 62
251
+ ],
252
+ "ɞ": [
253
+ 63
254
+ ],
255
+ "ɟ": [
256
+ 64
257
+ ],
258
+ "ɠ": [
259
+ 65
260
+ ],
261
+ "ɡ": [
262
+ 66
263
+ ],
264
+ "ɢ": [
265
+ 67
266
+ ],
267
+ "ɣ": [
268
+ 68
269
+ ],
270
+ "ɤ": [
271
+ 69
272
+ ],
273
+ "ɥ": [
274
+ 70
275
+ ],
276
+ "ɦ": [
277
+ 71
278
+ ],
279
+ "ɧ": [
280
+ 72
281
+ ],
282
+ "ɨ": [
283
+ 73
284
+ ],
285
+ "ɪ": [
286
+ 74
287
+ ],
288
+ "ɫ": [
289
+ 75
290
+ ],
291
+ "ɬ": [
292
+ 76
293
+ ],
294
+ "ɭ": [
295
+ 77
296
+ ],
297
+ "ɮ": [
298
+ 78
299
+ ],
300
+ "ɯ": [
301
+ 79
302
+ ],
303
+ "ɰ": [
304
+ 80
305
+ ],
306
+ "ɱ": [
307
+ 81
308
+ ],
309
+ "ɲ": [
310
+ 82
311
+ ],
312
+ "ɳ": [
313
+ 83
314
+ ],
315
+ "ɴ": [
316
+ 84
317
+ ],
318
+ "ɵ": [
319
+ 85
320
+ ],
321
+ "ɶ": [
322
+ 86
323
+ ],
324
+ "ɸ": [
325
+ 87
326
+ ],
327
+ "ɹ": [
328
+ 88
329
+ ],
330
+ "ɺ": [
331
+ 89
332
+ ],
333
+ "ɻ": [
334
+ 90
335
+ ],
336
+ "ɽ": [
337
+ 91
338
+ ],
339
+ "ɾ": [
340
+ 92
341
+ ],
342
+ "ʀ": [
343
+ 93
344
+ ],
345
+ "ʁ": [
346
+ 94
347
+ ],
348
+ "ʂ": [
349
+ 95
350
+ ],
351
+ "ʃ": [
352
+ 96
353
+ ],
354
+ "ʄ": [
355
+ 97
356
+ ],
357
+ "ʈ": [
358
+ 98
359
+ ],
360
+ "ʉ": [
361
+ 99
362
+ ],
363
+ "ʊ": [
364
+ 100
365
+ ],
366
+ "ʋ": [
367
+ 101
368
+ ],
369
+ "ʌ": [
370
+ 102
371
+ ],
372
+ "ʍ": [
373
+ 103
374
+ ],
375
+ "ʎ": [
376
+ 104
377
+ ],
378
+ "ʏ": [
379
+ 105
380
+ ],
381
+ "ʐ": [
382
+ 106
383
+ ],
384
+ "ʑ": [
385
+ 107
386
+ ],
387
+ "ʒ": [
388
+ 108
389
+ ],
390
+ "ʔ": [
391
+ 109
392
+ ],
393
+ "ʕ": [
394
+ 110
395
+ ],
396
+ "ʘ": [
397
+ 111
398
+ ],
399
+ "ʙ": [
400
+ 112
401
+ ],
402
+ "ʛ": [
403
+ 113
404
+ ],
405
+ "ʜ": [
406
+ 114
407
+ ],
408
+ "ʝ": [
409
+ 115
410
+ ],
411
+ "ʟ": [
412
+ 116
413
+ ],
414
+ "ʡ": [
415
+ 117
416
+ ],
417
+ "ʢ": [
418
+ 118
419
+ ],
420
+ "ʦ": [
421
+ 155
422
+ ],
423
+ "ʰ": [
424
+ 145
425
+ ],
426
+ "ʲ": [
427
+ 119
428
+ ],
429
+ "ˈ": [
430
+ 120
431
+ ],
432
+ "ˌ": [
433
+ 121
434
+ ],
435
+ "ː": [
436
+ 122
437
+ ],
438
+ "ˑ": [
439
+ 123
440
+ ],
441
+ "˞": [
442
+ 124
443
+ ],
444
+ "ˤ": [
445
+ 146
446
+ ],
447
+ "̃": [
448
+ 141
449
+ ],
450
+ "̧": [
451
+ 140
452
+ ],
453
+ "̩": [
454
+ 144
455
+ ],
456
+ "̪": [
457
+ 142
458
+ ],
459
+ "̯": [
460
+ 143
461
+ ],
462
+ "̺": [
463
+ 152
464
+ ],
465
+ "̻": [
466
+ 153
467
+ ],
468
+ "β": [
469
+ 125
470
+ ],
471
+ "ε": [
472
+ 147
473
+ ],
474
+ "θ": [
475
+ 126
476
+ ],
477
+ "χ": [
478
+ 127
479
+ ],
480
+ "ᵻ": [
481
+ 128
482
+ ],
483
+ "↑": [
484
+ 151
485
+ ],
486
+ "↓": [
487
+ 148
488
+ ],
489
+ "ⱱ": [
490
+ 129
491
+ ]
492
+ },
493
+ "num_symbols": 256,
494
+ "num_speakers": 1,
495
+ "speaker_id_map": {},
496
+ "piper_version": "1.0.0"
497
+ }
model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:80c90edca36003724c5b200be00301dea756001156d12b81036578f3d5bbc3b8
3
+ size 63511038
phonikud-1.0.int8.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5c4e7b0dbb263315ca124865da1ef3da3e91f64fb8acec6c437312a6bc0a8d51
3
+ size 307683244
phonikud_tts/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from piper_onnx import Piper
2
+ from phonikud import phonemize
3
+ from phonikud_onnx import Phonikud
4
+
5
+ class StyleTTS2:
6
+ def __init__(self):
7
+ ...
8
+
9
+ def create(text: str, is_phonemes = False):
10
+ ...
phonikud_tts/py.typed ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv export --no-hashes --no-emit-project
3
+ attrs==25.3.0
4
+ # via
5
+ # csvw
6
+ # jsonschema
7
+ # phonemizer-fork
8
+ # referencing
9
+ babel==2.17.0
10
+ # via csvw
11
+ certifi==2025.4.26
12
+ # via requests
13
+ cffi==1.17.1
14
+ # via
15
+ # sounddevice
16
+ # soundfile
17
+ charset-normalizer==3.4.2
18
+ # via requests
19
+ colorama==0.4.6
20
+ # via
21
+ # colorlog
22
+ # csvw
23
+ # tqdm
24
+ coloredlogs==15.0.1
25
+ # via onnxruntime
26
+ colorlog==6.9.0
27
+ # via phonikud
28
+ csvw==3.5.1
29
+ # via segments
30
+ dlinfo==2.0.0
31
+ # via phonemizer-fork
32
+ docopt==0.6.2
33
+ # via num2words
34
+ espeakng-loader==0.2.4
35
+ # via piper-onnx
36
+ filelock==3.18.0
37
+ # via huggingface-hub
38
+ flatbuffers==25.2.10
39
+ # via onnxruntime
40
+ fsspec==2025.5.1
41
+ # via huggingface-hub
42
+ hf-xet==1.1.2 ; platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'
43
+ # via huggingface-hub
44
+ huggingface-hub==0.32.2
45
+ # via tokenizers
46
+ humanfriendly==10.0
47
+ # via coloredlogs
48
+ idna==3.10
49
+ # via requests
50
+ isodate==0.7.2
51
+ # via csvw
52
+ joblib==1.5.1
53
+ # via phonemizer-fork
54
+ jsonschema==4.24.0
55
+ # via csvw
56
+ jsonschema-specifications==2025.4.1
57
+ # via jsonschema
58
+ language-tags==1.2.0
59
+ # via csvw
60
+ mpmath==1.3.0
61
+ # via sympy
62
+ num2words==0.5.14
63
+ # via phonikud
64
+ numpy==2.2.6
65
+ # via
66
+ # onnxruntime
67
+ # soundfile
68
+ onnxruntime==1.22.0
69
+ # via
70
+ # phonikud-onnx
71
+ # piper-onnx
72
+ packaging==25.0
73
+ # via
74
+ # huggingface-hub
75
+ # onnxruntime
76
+ phonemizer-fork==3.3.2
77
+ # via piper-onnx
78
+ phonikud @ git+https://github.com/thewh1teagle/phonikud@fe201c7c1840185a3b8d8b65cd1bfdb00af959e6
79
+ # via phonikud-tts
80
+ phonikud-onnx==1.0.1
81
+ # via phonikud-tts
82
+ piper-onnx==1.0.5
83
+ # via phonikud-tts
84
+ protobuf==6.31.1
85
+ # via onnxruntime
86
+ pycparser==2.22
87
+ # via cffi
88
+ pyparsing==3.2.3
89
+ # via rdflib
90
+ pyreadline3==3.5.4 ; sys_platform == 'win32'
91
+ # via humanfriendly
92
+ python-dateutil==2.9.0.post0
93
+ # via csvw
94
+ pyyaml==6.0.2
95
+ # via huggingface-hub
96
+ rdflib==7.1.4
97
+ # via csvw
98
+ referencing==0.36.2
99
+ # via
100
+ # jsonschema
101
+ # jsonschema-specifications
102
+ regex==2024.11.6
103
+ # via
104
+ # phonikud
105
+ # segments
106
+ requests==2.32.3
107
+ # via
108
+ # csvw
109
+ # huggingface-hub
110
+ rfc3986==1.5.0
111
+ # via csvw
112
+ rpds-py==0.25.1
113
+ # via
114
+ # jsonschema
115
+ # referencing
116
+ segments==2.3.0
117
+ # via phonemizer-fork
118
+ six==1.17.0
119
+ # via python-dateutil
120
+ sounddevice==0.5.2
121
+ # via phonikud-tts
122
+ soundfile==0.13.1
123
+ # via phonikud-tts
124
+ sympy==1.14.0
125
+ # via onnxruntime
126
+ tokenizers==0.21.1
127
+ # via phonikud-onnx
128
+ tqdm==4.67.1
129
+ # via huggingface-hub
130
+ typing-extensions==4.13.2
131
+ # via
132
+ # huggingface-hub
133
+ # phonemizer-fork
134
+ # referencing
135
+ uritemplate==4.1.1
136
+ # via csvw
137
+ urllib3==2.4.0
138
+ # via requests
139
+ Flask
static/script.js ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function hasDiacritics(text) {
2
+ // Check for Hebrew diacritics (nikud) in the range \u05b0 to \u05c7
3
+ return /[\u05b0-\u05c7]/.test(text);
4
+ }
5
+
6
+ function generate(mode) {
7
+ const statusId = mode + "-status";
8
+ const buttonId = mode + "-btn";
9
+ const statusElement = document.getElementById(statusId);
10
+ const buttonElement = document.getElementById(buttonId);
11
+ const alertElement = document.getElementById("diacritics-alert");
12
+
13
+ // Hide alert first
14
+ if (alertElement) {
15
+ alertElement.style.display = "none";
16
+ }
17
+
18
+ // Check for diacritics if in diacritics mode
19
+ if (mode === "diacritics") {
20
+ const text = document.getElementById("diacritics-input").value;
21
+ if (text && !hasDiacritics(text)) {
22
+ alertElement.style.display = "block";
23
+ statusElement.style.display = 'none'
24
+ return;
25
+ }
26
+ }
27
+
28
+ // Show and update status
29
+ statusElement.style.display = "flex";
30
+ statusElement.textContent = "Generating audio...";
31
+ statusElement.className = "status-indicator status-generating";
32
+
33
+ // Disable button
34
+ buttonElement.disabled = true;
35
+
36
+ // Hide audio section
37
+ const audioSection = document.getElementById("audio-section");
38
+ const audio = document.getElementById("audio");
39
+ audio.pause();
40
+ audio.src = "";
41
+
42
+ const formData = new FormData();
43
+ formData.append("mode", mode);
44
+ formData.append(
45
+ "text",
46
+ mode === "phonemes" ? "" : document.getElementById(mode + "-input").value
47
+ );
48
+ formData.append("phonemes", document.getElementById("phonemes-input").value);
49
+
50
+ fetch("/generate", { method: "POST", body: formData })
51
+ .then((res) => res.json())
52
+ .then((data) => {
53
+ // Update all fields with returned data
54
+ if (data.diacritics) {
55
+ document.getElementById("diacritics-input").value = data.diacritics || "";
56
+ }
57
+ if (data.phonemes) {
58
+ document.getElementById("phonemes-input").value = data.phonemes || "";
59
+ }
60
+
61
+
62
+
63
+ // Set up audio
64
+ audio.src = data.audio;
65
+ audioSection.style.display = "block";
66
+
67
+ // Auto-play audio
68
+ setTimeout(() => {
69
+ audio.play().catch(e => console.log("Auto-play prevented by browser"));
70
+ }, 300);
71
+
72
+ // Update status to success
73
+ statusElement.textContent = "✓ Audio generated successfully";
74
+ statusElement.className = "status-indicator status-ready";
75
+ })
76
+ .catch((err) => {
77
+ statusElement.textContent = "✗ Error generating audio";
78
+ statusElement.className = "status-indicator status-error";
79
+ console.error(err);
80
+ })
81
+ .finally(() => {
82
+ // Re-enable button
83
+ buttonElement.disabled = false;
84
+ });
85
+ }
86
+
87
+ const diacriticsInput = document.getElementById('diacritics-input');
88
+ const phonemesInput = document.getElementById('phonemes-input');
89
+ const textInput = document.getElementById('text-input');
90
+ const btnHatama = document.getElementById('btnHatama');
91
+ const btnVocalShva = document.getElementById('btnVocalShva');
92
+ const btnStress = document.getElementById('btnStress');
93
+
94
+ function insertAtCursor(charToInsert, inputName) {
95
+ const inputElement = inputName == 'diacritics' ? diacriticsInput : inputName == 'text' ? textInput : phonemesInput
96
+ const start = inputElement.selectionStart;
97
+ const end = inputElement.selectionEnd;
98
+ const text = inputElement.value;
99
+
100
+ inputElement.value = text.slice(0, start) + charToInsert + text.slice(end);
101
+ // Move cursor after inserted char
102
+ inputElement.selectionStart = inputElement.selectionEnd = start + charToInsert.length;
103
+ inputElement.focus();
104
+ }
105
+
106
+ btnHatama.addEventListener('click', () => {
107
+ insertAtCursor('\u05ab'); // Hebrew Shin Dot (Hatama)
108
+ });
109
+
110
+ btnVocalShva.addEventListener('click', () => {
111
+ insertAtCursor('\u05bd'); // Hebrew Vocal Shva
112
+ });
113
+
114
+ // btnStress.addEventListener('click', () => {
115
+ // insertAtCursor('\u05bd');
116
+ // })
117
+
118
+ // Set initial example text
119
+ window.addEventListener('load', () => {
120
+ setTimeout(() => {
121
+ document.getElementById("text-input").value =
122
+ "מה שבהגדרה משאיר את הכלכלה ההונגרית מאחור, אפילו ביחס למדינות כמו פולין.";
123
+ document.getElementById('diacritics-input').value = "מָה שֶׁבַּ|הַגְדָּרָה מַשְׁאִיר אֶת הַ|כַּלְכָּלָה הַ|הוּנְגָּרִית מֵאָחוֹר, אֲפִ֫ילּוּ בְּֽ|יַ֫חַס לִ|מְדִינוֹת כְּמוֹ פּוֹלִין."
124
+ document.getElementById('phonemes-input').value = "mˈa ʃebahaɡdaʁˈa maʃʔˈiʁ ʔˈet hakalkalˈa hahunɡaʁˈit meʔaχˈoʁ, ʔafˈilu bejˈaχas limdinˈot kmˈo polˈin."
125
+ }, 500);
126
+ });
127
+
128
+ // Tab switching enhancement
129
+ document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tab => {
130
+ tab.addEventListener('shown.bs.tab', function (event) {
131
+ // Hide all status indicators and alerts when switching tabs
132
+ document.querySelectorAll('.status-indicator').forEach(status => {
133
+ status.style.display = 'none';
134
+ });
135
+ document.querySelectorAll('.alert').forEach(alert => {
136
+ alert.style.display = 'none';
137
+ });
138
+ });
139
+ });
static/style.css ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #2563eb;
3
+ --secondary-color: #64748b;
4
+ --success-color: #059669;
5
+ --warning-color: #d97706;
6
+ --accent-color: #7c3aed;
7
+ --bg-gradient: linear-gradient(135deg, #1e293b 0%, #334155 100%);
8
+ --card-bg: rgba(255, 255, 255, 0.98);
9
+ --border-radius: 12px;
10
+ --shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
11
+ --hover-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
12
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ background: var(--bg-gradient);
21
+ min-height: 100vh;
22
+ font-family: "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
23
+ Roboto, sans-serif;
24
+ padding: 20px;
25
+ color: #1f2937;
26
+ }
27
+
28
+ .container {
29
+ max-width: 900px;
30
+ margin: 0 auto;
31
+ }
32
+
33
+ .header {
34
+ text-align: center;
35
+ margin-bottom: 32px;
36
+ color: white;
37
+ }
38
+
39
+ .header h2 {
40
+ font-size: 2.5rem;
41
+ font-weight: 700;
42
+ margin-bottom: 8px;
43
+ letter-spacing: -0.025em;
44
+ }
45
+
46
+ .header-subtitle {
47
+ font-size: 1rem;
48
+ opacity: 0.9;
49
+ margin-bottom: 20px;
50
+ font-weight: 400;
51
+ }
52
+
53
+ .github-link {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 8px;
57
+ color: white;
58
+ text-decoration: none;
59
+ background: rgba(255, 255, 255, 0.1);
60
+ padding: 6px 12px;
61
+ border-radius: 8px;
62
+ backdrop-filter: blur(10px);
63
+ border: 1px solid rgba(255, 255, 255, 0.15);
64
+ transition: var(--transition);
65
+ font-weight: 500;
66
+ }
67
+
68
+ .github-link:hover {
69
+ background: rgba(255, 255, 255, 0.2);
70
+ color: white;
71
+ transform: translateY(-1px);
72
+ }
73
+
74
+ .main-card {
75
+ background: var(--card-bg);
76
+ border-radius: var(--border-radius);
77
+ box-shadow: var(--shadow);
78
+ overflow: hidden;
79
+ backdrop-filter: blur(20px);
80
+ border: 1px solid rgba(255, 255, 255, 0.2);
81
+ transition: var(--transition);
82
+ }
83
+
84
+ .main-card:hover {
85
+ box-shadow: var(--hover-shadow);
86
+ }
87
+
88
+ .tabs-container {
89
+ border-bottom: 1px solid #e5e7eb;
90
+ background: #f8fafc;
91
+ padding: 0;
92
+ }
93
+
94
+ .nav-tabs {
95
+ border: none;
96
+ margin: 0;
97
+ display: flex;
98
+ }
99
+
100
+ .nav-tabs .nav-link {
101
+ border: none;
102
+ border-radius: 0;
103
+ padding: 16px 24px;
104
+ font-weight: 600;
105
+ color: var(--secondary-color);
106
+ transition: var(--transition);
107
+ position: relative;
108
+ background: transparent;
109
+ border-bottom: 3px solid transparent;
110
+ flex: 1;
111
+ text-align: center;
112
+ }
113
+
114
+ .nav-tabs .nav-link:hover {
115
+ color: var(--primary-color);
116
+ background-color: rgba(37, 99, 235, 0.05);
117
+ }
118
+
119
+ .nav-tabs .nav-link.active {
120
+ color: var(--primary-color);
121
+ background-color: white;
122
+ border-bottom: 3px solid var(--primary-color);
123
+ }
124
+
125
+ .nav-tabs .nav-link i {
126
+ margin-right: 8px;
127
+ }
128
+
129
+ .tab-content {
130
+ padding: 32px;
131
+ /* min-height: 60vh; */
132
+ display: flex;
133
+ flex-direction: column;
134
+ }
135
+
136
+ .input-section {
137
+ margin-bottom: 24px;
138
+ flex: 1;
139
+ display: flex;
140
+ flex-direction: column;
141
+ }
142
+
143
+ .section-title {
144
+ font-size: 1.25rem;
145
+ font-weight: 600;
146
+ color: #111827;
147
+ margin-bottom: 8px;
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 8px;
151
+ }
152
+
153
+ .section-description {
154
+ color: var(--secondary-color);
155
+ font-size: 0.95rem;
156
+ margin-bottom: 16px;
157
+ }
158
+
159
+ .input-group {
160
+ display: flex;
161
+ gap: 12px;
162
+ align-items: flex-start;
163
+ flex: 1;
164
+ }
165
+
166
+ .form-control {
167
+ border: 2px solid #e5e7eb;
168
+ border-radius: var(--border-radius);
169
+ padding: 12px 16px;
170
+ font-size: 1.7rem;
171
+ transition: var(--transition);
172
+ flex: 1;
173
+ min-height: 120px;
174
+ resize: vertical;
175
+ font-family: "Noto Sans", sans-serif;
176
+ font-weight: 400;
177
+ color: #333;
178
+ }
179
+
180
+ .form-control:focus {
181
+ border-color: var(--primary-color);
182
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
183
+ outline: none;
184
+ }
185
+
186
+ .rtl {
187
+ direction: rtl;
188
+ font-family: "Noto Sans", "Times New Roman", serif;
189
+ }
190
+
191
+ .phoneme-input {
192
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
193
+ font-size: 1.7rem;
194
+ }
195
+
196
+ .btn-generate {
197
+ background: var(--primary-color);
198
+ border: none;
199
+ border-radius: var(--border-radius);
200
+ color: white;
201
+ padding: 12px 24px;
202
+ font-weight: 600;
203
+ font-size: 14px;
204
+ min-width: 120px;
205
+ height: fit-content;
206
+ transition: var(--transition);
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 8px;
210
+ justify-content: center;
211
+ }
212
+
213
+ .btn-generate:hover {
214
+ background: #1d4ed8;
215
+ transform: translateY(-1px);
216
+ box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
217
+ }
218
+
219
+ .btn-generate:active {
220
+ transform: translateY(0);
221
+ }
222
+
223
+ .btn-generate:disabled {
224
+ opacity: 0.6;
225
+ cursor: not-allowed;
226
+ transform: none;
227
+ }
228
+
229
+ @import url('https://fonts.googleapis.com/css2?family=Alef:wght@400;700&display=swap');
230
+
231
+ #diacritics-input, #text-input {
232
+ font-family: 'Noto Sans';
233
+ font-weight: 100;
234
+ }
235
+
236
+ .status-indicator {
237
+ margin-top: 16px;
238
+ padding: 12px 16px;
239
+ border-radius: var(--border-radius);
240
+ font-weight: 500;
241
+ text-align: center;
242
+ font-size: 14px;
243
+ min-height: 44px;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ transition: var(--transition);
248
+ }
249
+
250
+ .status-generating {
251
+ background: linear-gradient(90deg, #fbbf24, #f59e0b);
252
+ color: white;
253
+ }
254
+
255
+ .status-ready {
256
+ background: linear-gradient(90deg, #10b981, #059669);
257
+ color: white;
258
+ }
259
+
260
+ .status-error {
261
+ background: linear-gradient(90deg, #ef4444, #dc2626);
262
+ color: white;
263
+ }
264
+
265
+ .helper-info {
266
+ background: #f1f5f9;
267
+ border: 1px solid #cbd5e1;
268
+ border-radius: var(--border-radius);
269
+ padding: 12px 16px;
270
+ font-size: 13px;
271
+ color: #475569;
272
+ margin-top: 12px;
273
+ display: flex;
274
+ align-items: center;
275
+ gap: 12px;
276
+ flex-wrap: wrap;
277
+ }
278
+
279
+ .helper-btn {
280
+ display: inline-flex;
281
+ align-items: center;
282
+ gap: 6px;
283
+ padding: 6px 12px;
284
+ font-family: "Noto Sans", sans-serif;
285
+ font-size: 0.85rem;
286
+ color: #444;
287
+ background-color: #f7f7f7;
288
+ border: 1.5px solid #bbb;
289
+ border-radius: var(--border-radius);
290
+ box-shadow: inset 0 -1px 0 #bbb, 0 2px 0 #ccc;
291
+ cursor: pointer;
292
+ user-select: none;
293
+ transition: var(--transition);
294
+ }
295
+
296
+ .helper-btn:hover {
297
+ background-color: #e7e7e7;
298
+ box-shadow: inset 0 -1px 0 #999, 0 3px 0 #aaa;
299
+ }
300
+
301
+ .audio-section {
302
+ background: #f8fafc;
303
+ border: 1px solid #e5e7eb;
304
+ border-radius: var(--border-radius);
305
+ padding: 24px;
306
+ text-align: center;
307
+ margin-top: 24px;
308
+ }
309
+
310
+ .audio-title {
311
+ font-size: 1.1rem;
312
+ font-weight: 600;
313
+ color: #111827;
314
+ margin-bottom: 16px;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ gap: 8px;
319
+ }
320
+
321
+ #audio {
322
+ width: 100%;
323
+ max-width: 400px;
324
+ border-radius: var(--border-radius);
325
+ }
326
+
327
+ .alert {
328
+ border-radius: var(--border-radius);
329
+ border: none;
330
+ padding: 12px 16px;
331
+ margin-top: 12px;
332
+ }
333
+
334
+ .alert-warning {
335
+ background: linear-gradient(90deg, #fbbf24, #f59e0b);
336
+ color: white;
337
+ }
338
+
339
+ /* Animations */
340
+ @keyframes fadeIn {
341
+ from {
342
+ opacity: 0;
343
+ transform: translateY(10px);
344
+ }
345
+ to {
346
+ opacity: 1;
347
+ transform: translateY(0);
348
+ }
349
+ }
350
+
351
+ .tab-pane {
352
+ animation: fadeIn 0.3s ease-out;
353
+ }
354
+
355
+ /* Mobile Responsive */
356
+ @media (max-width: 768px) {
357
+
358
+ body {
359
+ padding: 10px;
360
+ }
361
+
362
+
363
+
364
+ .container {
365
+ padding: 0;
366
+ }
367
+
368
+ .header h2 {
369
+ font-size: 2rem;
370
+ }
371
+
372
+ .header-subtitle {
373
+ font-size: 0.9rem;
374
+ }
375
+
376
+ .main-card {
377
+
378
+ min-height: 95vh;
379
+ display: flex;
380
+ flex-direction: column;
381
+ }
382
+
383
+ .nav-tabs {
384
+ display: flex;
385
+ flex-direction: row;
386
+ width: 100%;
387
+ }
388
+
389
+ .nav-tabs .nav-link {
390
+ padding: 14px 6px;
391
+ font-size: 14px;
392
+ flex: 1;
393
+ text-align: center;
394
+ min-width: 0;
395
+ word-wrap: break-word;
396
+ }
397
+
398
+ .nav-tabs .nav-link i {
399
+ margin-right: 4px;
400
+ }
401
+
402
+ .tab-content {
403
+ padding: 20px 16px;
404
+ /* flex: 1; */
405
+ /* overflow-y: auto; */
406
+ height: 100%;
407
+ min-height: 0;
408
+ }
409
+
410
+ .input-section {
411
+ height: 100%;
412
+ display: flex;
413
+ flex-direction: column;
414
+ }
415
+
416
+ .input-group {
417
+ display: flex;
418
+ flex: 1;
419
+ min-height: 0;
420
+ }
421
+
422
+ .form-control {
423
+ font-size: 1.3rem;
424
+ min-height: 200px;
425
+ flex: 1;
426
+ }
427
+
428
+ .btn-generate {
429
+ width: 100%;
430
+ margin-top: 12px;
431
+ }
432
+
433
+ .helper-info {
434
+ flex-direction: column;
435
+ align-items: stretch;
436
+ gap: 8px;
437
+ }
438
+
439
+ .helper-btn {
440
+ width: 100%;
441
+ justify-content: center;
442
+ }
443
+
444
+ .audio-section {
445
+ padding: 16px;
446
+ margin-top: 16px;
447
+ }
448
+ }
449
+
450
+ @media (max-width: 480px) {
451
+
452
+
453
+
454
+ .header h2 {
455
+ font-size: 1.75rem;
456
+ }
457
+
458
+ .nav-tabs .nav-link {
459
+ padding: 12px 4px;
460
+ font-size: 13px;
461
+ }
462
+
463
+ .tab-content {
464
+ padding: 16px 12px;
465
+ }
466
+
467
+ .section-title {
468
+ font-size: 1.1rem;
469
+ }
470
+
471
+ .section-description {
472
+ font-size: 0.9rem;
473
+ }
474
+ }
475
+
476
+
477
+ .main-card {
478
+ min-height: 65vh;
479
+ }
480
+
481
+
482
+ textarea {
483
+ min-height: 180px !important;
484
+ }
485
+
486
+ @media (max-width: 768px) {
487
+ .main-card {
488
+ min-height: 80vh;
489
+ }
490
+
491
+ textarea {
492
+ /* min-height: 50px !important; */
493
+ }
494
+ }
495
+
496
+
497
+ #phonemes-input {
498
+ font-family: 'Noto Sans', sans-serif;
499
+ }
500
+
501
+ .helper-phonemes {
502
+ display: flex;
503
+ flex-direction: row;
504
+ }
505
+
506
+ .helper-phonemes button {
507
+ width: unset;
508
+ }
509
+
510
+
511
+ @media (max-width: 768px) {
512
+
513
+ textarea {
514
+ height: 30vh !important;
515
+ }
516
+ }
templates/index.html ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Hebrew TTS with Phonikud</title>
7
+ <link
8
+ href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
9
+ rel="stylesheet"
10
+ />
11
+ <link
12
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
13
+ rel="stylesheet"
14
+ />
15
+ <link
16
+ href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;600&display=swap"
17
+ rel="stylesheet"
18
+ />
19
+ <link rel="stylesheet" href="static/style.css" />
20
+ </head>
21
+
22
+ <body>
23
+ <div class="container">
24
+ <!-- Header -->
25
+ <div class="header">
26
+ <h2><i class="fas fa-microphone-alt"></i> Hebrew TTS with Phonikud</h2>
27
+ <p class="header-subtitle">
28
+ Fast Text-to-Speech in Hebrew with Phonetic Control
29
+ </p>
30
+ </div>
31
+
32
+ <!-- Main Card with Tabs -->
33
+ <div class="main-card">
34
+ <!-- Tab Navigation -->
35
+ <div class="tabs-container">
36
+ <ul class="nav nav-tabs w-100 d-flex" role="tablist">
37
+ <li class="nav-item flex-fill">
38
+ <button
39
+ class="nav-link w-100 text-center active"
40
+ data-bs-toggle="tab"
41
+ data-bs-target="#text-tab"
42
+ type="button"
43
+ >
44
+ <i class="fas fa-font"></i> Text
45
+ </button>
46
+ </li>
47
+ <li class="nav-item flex-fill">
48
+ <button
49
+ class="nav-link w-100 text-center"
50
+ data-bs-toggle="tab"
51
+ data-bs-target="#diacritics-tab"
52
+ type="button"
53
+ >
54
+ <i class="fas fa-language"></i> Diacritics
55
+ </button>
56
+ </li>
57
+ <li class="nav-item flex-fill">
58
+ <button
59
+ class="nav-link w-100 text-center"
60
+ data-bs-toggle="tab"
61
+ data-bs-target="#phonemes-tab"
62
+ type="button"
63
+ >
64
+ <i class="fas fa-code"></i> Phonemes
65
+ </button>
66
+ </li>
67
+ </ul>
68
+ </div>
69
+
70
+ <!-- Tab Content -->
71
+ <div class="tab-content">
72
+ <!-- Hebrew Text Tab -->
73
+ <div class="tab-pane fade show active" id="text-tab">
74
+ <div class="input-section">
75
+ <h3 class="section-title">
76
+ <i class="fas fa-font"></i>
77
+ Hebrew Text Input
78
+ </h3>
79
+ <p class="section-description">
80
+ Enter unvocalized Hebrew text to generate speech.
81
+ </p>
82
+ <div class="input-group">
83
+ <textarea
84
+ autocomplete="off"
85
+ autocorrect="off"
86
+ autocapitalize="off"
87
+ spellcheck="false"
88
+ id="text-input"
89
+ class="form-control rtl w-100 "
90
+ placeholder="הכנס כאן את הטקסט העברי שלך..."
91
+ ></textarea>
92
+ </div>
93
+ </div>
94
+ <div>
95
+ <button
96
+ class="btn-generate"
97
+ onclick="generate('text')"
98
+ id="text-btn"
99
+ >
100
+ <i class="fas fa-play"></i>
101
+ Generate
102
+ </button>
103
+ <div
104
+ id="text-status"
105
+ class="status-indicator"
106
+ style="display: none"
107
+ ></div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Diacritics Tab -->
112
+ <div class="tab-pane fade" id="diacritics-tab">
113
+ <div class="input-section">
114
+ <h3 class="section-title">
115
+ <i class="fas fa-language"></i>
116
+ Text with Diacritics
117
+ </h3>
118
+ <p class="section-description">
119
+ Enter Hebrew text with vowel markings and enhanced symbols
120
+ indicating pronunciation.
121
+ </p>
122
+ <div class="input-group">
123
+ <textarea
124
+ autocomplete="off"
125
+ autocorrect="off"
126
+ autocapitalize="off"
127
+ spellcheck="false"
128
+ id="diacritics-input"
129
+ class="form-control rtl w-100"
130
+ placeholder="טקסט עם ניקוד יופיע כאן או הכנס ידנית..."
131
+ ></textarea>
132
+ <div>
133
+ <button
134
+ class="btn-generate"
135
+ onclick="generate('diacritics')"
136
+ id="diacritics-btn"
137
+ >
138
+ <i class="fas fa-play"></i>
139
+ Generate
140
+ </button>
141
+ <div
142
+ id="text-status"
143
+ class="status-indicator"
144
+ style="display: none"
145
+ ></div>
146
+ </div>
147
+ </div>
148
+ <div class="helper-info">
149
+ <button type="button" class="helper-btn" id="btnHatama" onclick="insertAtCursor('\u05ab', 'diacritics')">
150
+ <i class="fas fa-dot-circle"></i>
151
+ Press for Stress
152
+ </button>
153
+ <button type="button" class="helper-btn" id="btnVocalShva" onclick="insertAtCursor('\u05bd', 'diacritics')">
154
+ <i class="fas fa-circle"></i>
155
+ Press for Vocal Shva
156
+ </button>
157
+ </div>
158
+ <div
159
+ id="diacritics-status"
160
+ class="status-indicator"
161
+ style="display: none"
162
+ ></div>
163
+ <div
164
+ id="diacritics-alert"
165
+ class="alert alert-warning"
166
+ style="display: none"
167
+ >
168
+ <i class="fas fa-exclamation-triangle"></i>
169
+ Text must contain diacritics. Switch to the Text tab to generate
170
+ them automatically.
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <!-- Phonemes Tab -->
176
+ <div class="tab-pane fade" id="phonemes-tab">
177
+ <div class="input-section">
178
+ <h3 class="section-title">
179
+ <i class="fas fa-code"></i>
180
+ Phonetic Representation
181
+ </h3>
182
+ <p class="section-description">
183
+ Enter IPA phonemes directly to generate speech.
184
+ </p>
185
+ <div class="input-group">
186
+ <textarea
187
+ autocomplete="off"
188
+ autocorrect="off"
189
+ autocapitalize="off"
190
+ spellcheck="false"
191
+ id="phonemes-input"
192
+ class="form-control w-100 phoneme-input"
193
+ placeholder="Phonetic transcription will appear here or enter manually..."
194
+ ></textarea>
195
+ <div>
196
+ <button
197
+ class="btn-generate"
198
+ onclick="generate('phonemes')"
199
+ id="phonemes-btn"
200
+ >
201
+ <i class="fas fa-play"></i>
202
+ Generate
203
+ </button>
204
+ <div
205
+ id="text-status"
206
+ class="status-indicator"
207
+ style="display: none"
208
+ ></div>
209
+ </div>
210
+ </div>
211
+ <div class="helper-info helper-phonemes">
212
+ <button
213
+ type="button"
214
+ class="helper-btn"
215
+ id="btnStress"
216
+ onclick="insertAtCursor('\u02c8', 'phonemes')"
217
+ >
218
+ <i class="fas fa-dot-circle"></i>
219
+ Stress ˈ
220
+ </button>
221
+
222
+ <button
223
+ type="button"
224
+ class="helper-btn"
225
+ id="btnStress"
226
+ onclick="insertAtCursor('χ', 'phonemes')"
227
+ >
228
+ <i class="fas fa-dot-circle"></i>
229
+ χ
230
+ </button>
231
+ <button
232
+ type="button"
233
+ class="helper-btn"
234
+ id="btnStress"
235
+ onclick="insertAtCursor('ʃ', 'phonemes')"
236
+ >
237
+ <i class="fas fa-dot-circle"></i>
238
+ ʃ
239
+ </button>
240
+
241
+ <button
242
+ type="button"
243
+ class="helper-btn"
244
+ id="btnStress"
245
+ onclick="insertAtCursor('ʔ', 'phonemes')"
246
+ >
247
+ <i class="fas fa-dot-circle"></i>
248
+ ʔ
249
+ </button>
250
+
251
+ <button
252
+ type="button"
253
+ class="helper-btn"
254
+ id="btnStress"
255
+ onclick="insertAtCursor('χ', 'phonemes')"
256
+ >
257
+ <i class="fas fa-dot-circle"></i>
258
+ χ
259
+ </button>
260
+
261
+ <button
262
+ type="button"
263
+ class="helper-btn"
264
+ id="btnStress"
265
+ onclick="insertAtCursor('ʁ', 'phonemes')"
266
+ >
267
+ <i class="fas fa-dot-circle"></i>
268
+ ʁ
269
+ </button>
270
+
271
+ <button
272
+ type="button"
273
+ class="helper-btn"
274
+ id="btnStress"
275
+ onclick="insertAtCursor('ʒ', 'phonemes')"
276
+ >
277
+ <i class="fas fa-dot-circle"></i>
278
+ ʒ
279
+ </button>
280
+
281
+ <button
282
+ type="button"
283
+ class="helper-btn"
284
+ id="btnStress"
285
+ onclick="insertAtCursor('ɡ', 'phonemes')"
286
+ >
287
+ <i class="fas fa-dot-circle"></i>
288
+ ɡ
289
+ </button>
290
+ </div>
291
+ <div
292
+ id="phonemes-status"
293
+ class="status-indicator"
294
+ style="display: none"
295
+ ></div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+
300
+ <!-- Audio Result Section -->
301
+ <div class="audio-section" id="audio-section" style="display: none">
302
+ <h3 class="audio-title">
303
+ <i class="fas fa-volume-up"></i>
304
+ Generated Audio
305
+ </h3>
306
+ <audio id="audio" controls></audio>
307
+ </div>
308
+ </div>
309
+ </div>
310
+
311
+ <div class="container text-center mt-4">
312
+ <a
313
+ href="https://github.com/thewh1teagle/phonikud"
314
+ class="github-link"
315
+ target="_blank"
316
+ >
317
+ <i class="fab fa-github"></i>
318
+ View Phonikud on GitHub
319
+ </a>
320
+ </div>
321
+
322
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
323
+ <script src="static/script.js"></script>
324
+ </body>
325
+ </html>