profplate commited on
Commit
171ca6d
·
verified ·
1 Parent(s): 739b228

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +411 -0
app.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ IPA Chart Explorer — Interactive visual IPA chart with Spanish highlighting.
3
+
4
+ Click any IPA symbol to see its articulatory description, whether it exists
5
+ in Spanish, and which other languages use it.
6
+ """
7
+
8
+ import gradio as gr
9
+ from ipa_data import (
10
+ CONSONANTS, VOWELS,
11
+ CONSONANT_PLACES, CONSONANT_MANNERS,
12
+ VOWEL_HEIGHTS, VOWEL_BACKNESSES,
13
+ VOWEL_TRAPEZOID_COORDS,
14
+ get_consonant_at, get_phoneme_info,
15
+ )
16
+
17
+ print("IPA Chart Explorer loading...")
18
+
19
+
20
+ # =============================================================================
21
+ # HTML/CSS FOR THE CONSONANT CHART
22
+ # =============================================================================
23
+
24
+ def build_consonant_table_html(spanish_only=False):
25
+ """Build the full IPA consonant chart as an HTML table."""
26
+
27
+ # Place labels (abbreviated for column headers)
28
+ place_labels = {
29
+ "bilabial": "Bilabial",
30
+ "labiodental": "Labio-\ndental",
31
+ "dental": "Dental",
32
+ "alveolar": "Alveolar",
33
+ "postalveolar": "Post-\nalveolar",
34
+ "retroflex": "Retroflex",
35
+ "palatal": "Palatal",
36
+ "velar": "Velar",
37
+ "uvular": "Uvular",
38
+ "pharyngeal": "Pharyn-\ngeal",
39
+ "glottal": "Glottal",
40
+ }
41
+
42
+ rows_html = []
43
+
44
+ # Header row
45
+ header_cells = '<th class="manner-header"></th>'
46
+ for place in CONSONANT_PLACES:
47
+ label = place_labels[place].replace("\n", "<br>")
48
+ header_cells += f'<th class="place-header" colspan="2">{label}</th>'
49
+ rows_html.append(f"<tr>{header_cells}</tr>")
50
+
51
+ # Sub-header for voiceless/voiced
52
+ sub_cells = '<th class="manner-header"></th>'
53
+ for _ in CONSONANT_PLACES:
54
+ sub_cells += '<th class="voicing-sub">vl</th><th class="voicing-sub">vd</th>'
55
+ rows_html.append(f"<tr>{sub_cells}</tr>")
56
+
57
+ # Data rows
58
+ for manner in CONSONANT_MANNERS:
59
+ manner_label = manner.replace("/", " / ").title()
60
+ cells = f'<th class="manner-header">{manner_label}</th>'
61
+
62
+ for place in CONSONANT_PLACES:
63
+ for voicing in ["voiceless", "voiced"]:
64
+ symbol = get_consonant_at(place, manner, voicing)
65
+ if symbol:
66
+ info = CONSONANTS[symbol]
67
+ is_spanish = info["spanish"]
68
+
69
+ if spanish_only and not is_spanish:
70
+ cells += '<td class="ipa-cell empty"></td>'
71
+ continue
72
+
73
+ css_class = "ipa-cell spanish" if is_spanish else "ipa-cell"
74
+ cells += (
75
+ f'<td class="{css_class}" '
76
+ f'data-symbol="{symbol}" '
77
+ f'onclick="selectPhoneme(\'{symbol}\')" '
78
+ f'title="{info["name"]}">'
79
+ f'{symbol}</td>'
80
+ )
81
+ else:
82
+ cells += '<td class="ipa-cell empty"></td>'
83
+
84
+ rows_html.append(f"<tr>{cells}</tr>")
85
+
86
+ return f'<table class="consonant-chart">{"".join(rows_html)}</table>'
87
+
88
+
89
+ # =============================================================================
90
+ # SVG FOR THE VOWEL TRAPEZOID
91
+ # =============================================================================
92
+
93
+ def build_vowel_trapezoid_svg(spanish_only=False):
94
+ """Build the IPA vowel trapezoid as an inline SVG."""
95
+
96
+ # Trapezoid outline points
97
+ trapezoid_path = "M 80,30 L 420,30 L 350,370 L 205,370 Z"
98
+
99
+ # Grid lines (dashed)
100
+ grid_lines = []
101
+ # Horizontal lines for height levels
102
+ height_y = {"close": 40, "near-close": 95, "close-mid": 145, "mid": 200,
103
+ "open-mid": 250, "near-open": 305, "open": 355}
104
+ for height, y_val in height_y.items():
105
+ # Calculate x endpoints based on trapezoid slope
106
+ left_x = 80 + (y_val - 30) * (205 - 80) / (370 - 30)
107
+ right_x = 420 + (y_val - 30) * (350 - 420) / (370 - 30)
108
+ grid_lines.append(
109
+ f'<line x1="{left_x}" y1="{y_val}" x2="{right_x}" y2="{y_val}" '
110
+ f'class="grid-line"/>'
111
+ )
112
+
113
+ # Vertical-ish lines for backness
114
+ grid_lines.append('<line x1="250" y1="30" x2="275" y2="370" class="grid-line"/>')
115
+
116
+ # Height labels (left side)
117
+ height_labels = []
118
+ for height, y_val in height_y.items():
119
+ label = height.replace("-", "\u2011") # non-breaking hyphen
120
+ height_labels.append(
121
+ f'<text x="5" y="{y_val + 5}" class="axis-label">{label}</text>'
122
+ )
123
+
124
+ # Backness labels (top)
125
+ backness_labels = [
126
+ '<text x="80" y="20" class="axis-label" text-anchor="middle">Front</text>',
127
+ '<text x="250" y="20" class="axis-label" text-anchor="middle">Central</text>',
128
+ '<text x="420" y="20" class="axis-label" text-anchor="middle">Back</text>',
129
+ ]
130
+
131
+ # Vowel symbols
132
+ vowel_elements = []
133
+ for symbol, data in VOWELS.items():
134
+ pos_key = (data["height"], data["backness"])
135
+ if pos_key not in VOWEL_TRAPEZOID_COORDS:
136
+ continue
137
+
138
+ if spanish_only and not data["spanish"]:
139
+ continue
140
+
141
+ x, y = VOWEL_TRAPEZOID_COORDS[pos_key]
142
+
143
+ # Offset: unrounded left, rounded right
144
+ if data["rounding"] == "unrounded":
145
+ x -= 15
146
+ else:
147
+ x += 15
148
+
149
+ is_spanish = data["spanish"]
150
+ css_class = "vowel-symbol spanish" if is_spanish else "vowel-symbol"
151
+
152
+ vowel_elements.append(
153
+ f'<text x="{x}" y="{y}" class="{css_class}" '
154
+ f'data-symbol="{symbol}" '
155
+ f'onclick="selectPhoneme(\'{symbol}\')" '
156
+ f'style="cursor:pointer">'
157
+ f'{symbol}</text>'
158
+ )
159
+
160
+ svg = f"""
161
+ <svg viewBox="-10 0 500 400" class="vowel-trapezoid" xmlns="http://www.w3.org/2000/svg">
162
+ <path d="{trapezoid_path}" class="trapezoid-outline"/>
163
+ {"".join(grid_lines)}
164
+ {"".join(height_labels)}
165
+ {"".join(backness_labels)}
166
+ {"".join(vowel_elements)}
167
+ </svg>
168
+ """
169
+ return svg
170
+
171
+
172
+ # =============================================================================
173
+ # COMBINED HTML PAGE
174
+ # =============================================================================
175
+
176
+ CSS = """
177
+ <style>
178
+ .ipa-explorer { font-family: 'Segoe UI', system-ui, sans-serif; max-width: 1100px; margin: 0 auto; }
179
+ .chart-section { margin-bottom: 30px; }
180
+ .chart-title { font-size: 1.3em; font-weight: 600; margin-bottom: 10px; color: #333; }
181
+
182
+ /* Consonant chart */
183
+ .consonant-chart { border-collapse: collapse; width: 100%; font-size: 0.85em; }
184
+ .consonant-chart th, .consonant-chart td { border: 1px solid #ccc; padding: 4px 6px; text-align: center; }
185
+ .place-header { background: #f0f0f0; font-size: 0.8em; font-weight: 600; min-width: 50px; }
186
+ .manner-header { background: #f0f0f0; font-weight: 600; text-align: right !important; padding-right: 8px !important; white-space: nowrap; }
187
+ .voicing-sub { background: #f8f8f8; font-size: 0.7em; color: #888; font-style: italic; }
188
+
189
+ .ipa-cell { font-size: 1.3em; cursor: pointer; padding: 6px !important; transition: all 0.15s; }
190
+ .ipa-cell:hover { background: #e3f2fd; transform: scale(1.1); }
191
+ .ipa-cell.spanish { background: #e8f5e9; font-weight: 600; }
192
+ .ipa-cell.spanish:hover { background: #c8e6c9; }
193
+ .ipa-cell.empty { background: #fafafa; cursor: default; }
194
+ .ipa-cell.selected { background: #bbdefb !important; outline: 2px solid #1976d2; }
195
+
196
+ /* Vowel trapezoid */
197
+ .vowel-trapezoid { max-width: 500px; margin: 0 auto; display: block; }
198
+ .trapezoid-outline { fill: none; stroke: #999; stroke-width: 1.5; }
199
+ .grid-line { stroke: #ddd; stroke-width: 0.5; stroke-dasharray: 4 4; }
200
+ .axis-label { font-size: 11px; fill: #666; font-family: sans-serif; }
201
+
202
+ .vowel-symbol { font-size: 18px; fill: #333; font-family: 'Noto Sans', sans-serif; text-anchor: middle; dominant-baseline: middle; }
203
+ .vowel-symbol:hover { fill: #1976d2; font-size: 22px; }
204
+ .vowel-symbol.spanish { fill: #2e7d32; font-weight: bold; }
205
+ .vowel-symbol.spanish:hover { fill: #1b5e20; }
206
+ .vowel-symbol.selected { fill: #1976d2 !important; font-size: 22px; }
207
+
208
+ /* Info panel */
209
+ .info-panel { background: #f5f5f5; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-top: 15px; min-height: 100px; }
210
+ .info-symbol { font-size: 3em; font-weight: bold; margin-bottom: 10px; }
211
+ .info-symbol.is-spanish { color: #2e7d32; }
212
+ .info-name { font-size: 1.1em; color: #555; margin-bottom: 15px; }
213
+ .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
214
+ .info-item { padding: 8px 12px; background: white; border-radius: 4px; border: 1px solid #eee; }
215
+ .info-label { font-size: 0.8em; color: #888; text-transform: uppercase; letter-spacing: 0.05em; }
216
+ .info-value { font-size: 1em; color: #333; margin-top: 2px; }
217
+ .spanish-badge { display: inline-block; background: #e8f5e9; color: #2e7d32; padding: 2px 10px; border-radius: 12px; font-size: 0.85em; font-weight: 600; }
218
+ .not-spanish-badge { display: inline-block; background: #f5f5f5; color: #999; padding: 2px 10px; border-radius: 12px; font-size: 0.85em; }
219
+
220
+ /* Legend */
221
+ .legend { display: flex; gap: 20px; margin-bottom: 15px; font-size: 0.9em; }
222
+ .legend-item { display: flex; align-items: center; gap: 6px; }
223
+ .legend-swatch { width: 16px; height: 16px; border-radius: 3px; border: 1px solid #ccc; }
224
+ .legend-swatch.spanish { background: #e8f5e9; }
225
+ .legend-swatch.other { background: white; }
226
+ </style>
227
+ """
228
+
229
+ JAVASCRIPT = """
230
+ <script>
231
+ // Store all phoneme data passed from Python
232
+ let phonemeData = PHONEME_DATA_PLACEHOLDER;
233
+
234
+ function selectPhoneme(symbol) {
235
+ // Remove previous selection
236
+ document.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'));
237
+
238
+ // Highlight selected cell
239
+ document.querySelectorAll(`[data-symbol="${symbol}"]`).forEach(el => {
240
+ el.classList.add('selected');
241
+ });
242
+
243
+ // Update info panel
244
+ let data = phonemeData[symbol];
245
+ if (!data) return;
246
+
247
+ let panel = document.getElementById('info-panel');
248
+ let spanishHtml = data.spanish
249
+ ? `<span class="spanish-badge">Used in Spanish</span>`
250
+ : `<span class="not-spanish-badge">Not in Spanish</span>`;
251
+
252
+ let spanishExample = data.spanish && data.spanish_example
253
+ ? `<div class="info-item"><div class="info-label">Spanish Example</div><div class="info-value">${data.spanish_example}</div></div>`
254
+ : '';
255
+
256
+ let symbolClass = data.spanish ? 'info-symbol is-spanish' : 'info-symbol';
257
+ let typeLabel = data.type === 'consonant' ? 'Consonant' : 'Vowel';
258
+
259
+ let featureHtml = '';
260
+ if (data.type === 'consonant') {
261
+ featureHtml = `
262
+ <div class="info-item"><div class="info-label">Place</div><div class="info-value">${data.place}</div></div>
263
+ <div class="info-item"><div class="info-label">Manner</div><div class="info-value">${data.manner}</div></div>
264
+ <div class="info-item"><div class="info-label">Voicing</div><div class="info-value">${data.voicing}</div></div>
265
+ `;
266
+ } else {
267
+ featureHtml = `
268
+ <div class="info-item"><div class="info-label">Height</div><div class="info-value">${data.height}</div></div>
269
+ <div class="info-item"><div class="info-label">Backness</div><div class="info-value">${data.backness}</div></div>
270
+ <div class="info-item"><div class="info-label">Rounding</div><div class="info-value">${data.rounding}</div></div>
271
+ `;
272
+ }
273
+
274
+ panel.innerHTML = `
275
+ <div class="${symbolClass}">/${symbol}/</div>
276
+ <div class="info-name">${data.name} ${spanishHtml}</div>
277
+ <div class="info-grid">
278
+ <div class="info-item"><div class="info-label">Type</div><div class="info-value">${typeLabel}</div></div>
279
+ ${featureHtml}
280
+ ${spanishExample}
281
+ <div class="info-item"><div class="info-label">Also Found In</div><div class="info-value">${data.languages.join(', ')}</div></div>
282
+ </div>
283
+ `;
284
+ }
285
+ </script>
286
+ """
287
+
288
+
289
+ def build_phoneme_data_json():
290
+ """Build a JSON-compatible dict of all phoneme data for JavaScript."""
291
+ import json
292
+ data = {}
293
+ for sym, info in CONSONANTS.items():
294
+ data[sym] = {
295
+ "type": "consonant",
296
+ "name": info["name"],
297
+ "place": info["place"],
298
+ "manner": info["manner"],
299
+ "voicing": info["voicing"],
300
+ "spanish": info["spanish"],
301
+ "spanish_example": info["spanish_example"],
302
+ "languages": info["languages"],
303
+ }
304
+ for sym, info in VOWELS.items():
305
+ data[sym] = {
306
+ "type": "vowel",
307
+ "name": info["name"],
308
+ "height": info["height"],
309
+ "backness": info["backness"],
310
+ "rounding": info["rounding"],
311
+ "spanish": info["spanish"],
312
+ "spanish_example": info["spanish_example"],
313
+ "languages": info["languages"],
314
+ }
315
+ return json.dumps(data)
316
+
317
+
318
+ def build_full_page(spanish_only=False):
319
+ """Build the complete HTML page with charts and info panel."""
320
+ import json
321
+
322
+ consonant_html = build_consonant_table_html(spanish_only)
323
+ vowel_svg = build_vowel_trapezoid_svg(spanish_only)
324
+ phoneme_json = build_phoneme_data_json()
325
+
326
+ js = JAVASCRIPT.replace("PHONEME_DATA_PLACEHOLDER", phoneme_json)
327
+
328
+ mode_label = "Spanish Phonemes" if spanish_only else "Full IPA"
329
+
330
+ html = f"""
331
+ {CSS}
332
+ {js}
333
+ <div class="ipa-explorer">
334
+ <div class="legend">
335
+ <div class="legend-item">
336
+ <div class="legend-swatch spanish"></div>
337
+ <span>Spanish phoneme</span>
338
+ </div>
339
+ <div class="legend-item">
340
+ <div class="legend-swatch other"></div>
341
+ <span>Other languages</span>
342
+ </div>
343
+ <div style="margin-left: auto; color: #888; font-style: italic;">
344
+ Showing: {mode_label} &mdash; Click any symbol for details
345
+ </div>
346
+ </div>
347
+
348
+ <div class="chart-section">
349
+ <div class="chart-title">Consonants (Place &times; Manner of Articulation)</div>
350
+ {consonant_html}
351
+ </div>
352
+
353
+ <div class="chart-section">
354
+ <div class="chart-title">Vowels (Height &times; Backness)</div>
355
+ {vowel_svg}
356
+ </div>
357
+
358
+ <div id="info-panel" class="info-panel">
359
+ <div style="color: #999; font-style: italic; text-align: center; padding: 30px;">
360
+ Click any IPA symbol above to see its details
361
+ </div>
362
+ </div>
363
+ </div>
364
+ """
365
+ return html
366
+
367
+
368
+ # =============================================================================
369
+ # GRADIO APP
370
+ # =============================================================================
371
+
372
+ def update_chart(mode):
373
+ """Rebuild chart based on selected display mode."""
374
+ spanish_only = (mode == "Spanish Only")
375
+ return build_full_page(spanish_only)
376
+
377
+
378
+ with gr.Blocks(
379
+ title="IPA Chart Explorer",
380
+ theme=gr.themes.Soft(),
381
+ ) as demo:
382
+ gr.Markdown(
383
+ "# IPA Chart Explorer\n"
384
+ "Interactive International Phonetic Alphabet chart. "
385
+ "Spanish phonemes are highlighted in green. "
386
+ "Click any symbol to see where it lives in the human sound system."
387
+ )
388
+
389
+ mode = gr.Radio(
390
+ choices=["Full IPA", "Spanish Only"],
391
+ value="Full IPA",
392
+ label="Display Mode",
393
+ )
394
+
395
+ chart_html = gr.HTML(value=build_full_page(spanish_only=False))
396
+
397
+ mode.change(fn=update_chart, inputs=[mode], outputs=[chart_html])
398
+
399
+ gr.Markdown(
400
+ "---\n"
401
+ "**How to read the consonant chart:** Columns = where in the mouth "
402
+ "(lips → throat). Rows = how the air flows (stopped, through the nose, etc.). "
403
+ "Each cell has two slots: voiceless (left) and voiced (right).\n\n"
404
+ "**How to read the vowel trapezoid:** Top = tongue high, bottom = tongue low. "
405
+ "Left = tongue forward, right = tongue back. Pairs show unrounded (left) and "
406
+ "rounded (right) at each position."
407
+ )
408
+
409
+
410
+ print("IPA Chart Explorer ready!")
411
+ demo.launch()