"""
IPA Chart Explorer — Interactive visual IPA chart with Spanish highlighting.
Click any IPA symbol to see its articulatory description, whether it exists
in Spanish, and which other languages use it.
"""
import gradio as gr
from ipa_data import (
CONSONANTS, VOWELS,
CONSONANT_PLACES, CONSONANT_MANNERS,
VOWEL_HEIGHTS, VOWEL_BACKNESSES,
VOWEL_TRAPEZOID_COORDS,
get_consonant_at, get_phoneme_info,
)
print("IPA Chart Explorer loading...")
# =============================================================================
# HTML/CSS FOR THE CONSONANT CHART
# =============================================================================
def build_consonant_table_html(spanish_only=False):
"""Build the full IPA consonant chart as an HTML table."""
# Place labels (abbreviated for column headers)
place_labels = {
"bilabial": "Bilabial",
"labiodental": "Labio-\ndental",
"dental": "Dental",
"alveolar": "Alveolar",
"postalveolar": "Post-\nalveolar",
"retroflex": "Retroflex",
"palatal": "Palatal",
"velar": "Velar",
"uvular": "Uvular",
"pharyngeal": "Pharyn-\ngeal",
"glottal": "Glottal",
}
rows_html = []
# Header row
header_cells = '
'
for place in CONSONANT_PLACES:
label = place_labels[place].replace("\n", " ")
header_cells += f'
{label}
'
rows_html.append(f"
{header_cells}
")
# Sub-header for voiceless/voiced
sub_cells = '
'
for _ in CONSONANT_PLACES:
sub_cells += '
vl
vd
'
rows_html.append(f"
{sub_cells}
")
# Data rows
for manner in CONSONANT_MANNERS:
manner_label = manner.replace("/", " / ").title()
cells = f'
{manner_label}
'
for place in CONSONANT_PLACES:
for voicing in ["voiceless", "voiced"]:
symbol = get_consonant_at(place, manner, voicing)
if symbol:
info = CONSONANTS[symbol]
is_spanish = info["spanish"]
if spanish_only and not is_spanish:
cells += '
'
continue
css_class = "ipa-cell spanish" if is_spanish else "ipa-cell"
cells += (
f'
'
f'{symbol}
'
)
else:
cells += '
'
rows_html.append(f"
{cells}
")
return f'
{"".join(rows_html)}
'
# =============================================================================
# SVG FOR THE VOWEL TRAPEZOID
# =============================================================================
def build_vowel_trapezoid_svg(spanish_only=False):
"""Build the IPA vowel trapezoid as an inline SVG."""
# Trapezoid outline points
trapezoid_path = "M 80,30 L 420,30 L 350,370 L 205,370 Z"
# Grid lines (dashed)
grid_lines = []
# Horizontal lines for height levels
height_y = {"close": 40, "near-close": 95, "close-mid": 145, "mid": 200,
"open-mid": 250, "near-open": 305, "open": 355}
for height, y_val in height_y.items():
# Calculate x endpoints based on trapezoid slope
left_x = 80 + (y_val - 30) * (205 - 80) / (370 - 30)
right_x = 420 + (y_val - 30) * (350 - 420) / (370 - 30)
grid_lines.append(
f''
)
# Vertical-ish lines for backness
grid_lines.append('')
# Height labels (left side)
height_labels = []
for height, y_val in height_y.items():
label = height.replace("-", "\u2011") # non-breaking hyphen
height_labels.append(
f'{label}'
)
# Backness labels (top)
backness_labels = [
'Front',
'Central',
'Back',
]
# Vowel symbols
vowel_elements = []
for symbol, data in VOWELS.items():
pos_key = (data["height"], data["backness"])
if pos_key not in VOWEL_TRAPEZOID_COORDS:
continue
if spanish_only and not data["spanish"]:
continue
x, y = VOWEL_TRAPEZOID_COORDS[pos_key]
# Offset: unrounded left, rounded right
if data["rounding"] == "unrounded":
x -= 15
else:
x += 15
is_spanish = data["spanish"]
css_class = "vowel-symbol spanish" if is_spanish else "vowel-symbol"
vowel_elements.append(
f''
f'{symbol}'
)
svg = f"""
"""
return svg
# =============================================================================
# COMBINED HTML PAGE
# =============================================================================
CSS = """
"""
JAVASCRIPT = """
"""
def build_phoneme_data_json():
"""Build a JSON-compatible dict of all phoneme data for JavaScript."""
import json
data = {}
for sym, info in CONSONANTS.items():
data[sym] = {
"type": "consonant",
"name": info["name"],
"place": info["place"],
"manner": info["manner"],
"voicing": info["voicing"],
"spanish": info["spanish"],
"spanish_example": info["spanish_example"],
"languages": info["languages"],
}
for sym, info in VOWELS.items():
data[sym] = {
"type": "vowel",
"name": info["name"],
"height": info["height"],
"backness": info["backness"],
"rounding": info["rounding"],
"spanish": info["spanish"],
"spanish_example": info["spanish_example"],
"languages": info["languages"],
}
return json.dumps(data)
def build_full_page(spanish_only=False):
"""Build the complete HTML page with charts and info panel."""
import json
consonant_html = build_consonant_table_html(spanish_only)
vowel_svg = build_vowel_trapezoid_svg(spanish_only)
phoneme_json = build_phoneme_data_json()
js = JAVASCRIPT.replace("PHONEME_DATA_PLACEHOLDER", phoneme_json)
mode_label = "Spanish Phonemes" if spanish_only else "Full IPA"
html = f"""
{CSS}
{js}
Spanish phoneme
Other languages
Showing: {mode_label} — Click any symbol for details
Consonants (Place × Manner of Articulation)
{consonant_html}
Vowels (Height × Backness)
{vowel_svg}
Click any IPA symbol above to see its details
"""
return html
# =============================================================================
# GRADIO APP
# =============================================================================
def update_chart(mode):
"""Rebuild chart based on selected display mode."""
spanish_only = (mode == "Spanish Only")
return build_full_page(spanish_only)
with gr.Blocks(
title="IPA Chart Explorer",
theme=gr.themes.Soft(),
) as demo:
gr.Markdown(
"# IPA Chart Explorer\n"
"Interactive International Phonetic Alphabet chart. "
"Spanish phonemes are highlighted in green. "
"Click any symbol to see where it lives in the human sound system."
)
mode = gr.Radio(
choices=["Full IPA", "Spanish Only"],
value="Full IPA",
label="Display Mode",
)
chart_html = gr.HTML(value=build_full_page(spanish_only=False))
mode.change(fn=update_chart, inputs=[mode], outputs=[chart_html])
gr.Markdown(
"---\n"
"**How to read the consonant chart:** Columns = where in the mouth "
"(lips → throat). Rows = how the air flows (stopped, through the nose, etc.). "
"Each cell has two slots: voiceless (left) and voiced (right).\n\n"
"**How to read the vowel trapezoid:** Top = tongue high, bottom = tongue low. "
"Left = tongue forward, right = tongue back. Pairs show unrounded (left) and "
"rounded (right) at each position."
)
print("IPA Chart Explorer ready!")
demo.launch()