| import streamlit as st |
| import pyphen |
| import re |
| from typing import List, Tuple |
| import string |
| import os |
|
|
| |
| dic = pyphen.Pyphen(lang='en') |
|
|
| |
| st.set_page_config( |
| page_title="Text Pronunciation Analyzer", |
| page_icon="๐ฃ๏ธ", |
| layout="wide", |
| initial_sidebar_state="collapsed" |
| ) |
|
|
| |
| st.markdown(""" |
| <style> |
| .main-header { |
| background: linear-gradient(90deg, #6e8efb, #a777e3); |
| -webkit-background-clip: text; |
| background-clip: text; |
| color: transparent; |
| text-align: center; |
| font-size: 3rem; |
| font-weight: bold; |
| margin-bottom: 1rem; |
| } |
| |
| .subtitle { |
| text-align: center; |
| color: #666; |
| font-size: 1.2rem; |
| margin-bottom: 2rem; |
| } |
| |
| .word-highlight { |
| display: inline-block; |
| padding: 2px 4px; |
| margin: 0 1px; |
| border-radius: 4px; |
| transition: all 0.3s ease; |
| cursor: pointer; |
| color: #666; |
| background-color: transparent; |
| } |
| |
| .word-highlight:hover { |
| transform: translateY(-1px); |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
| } |
| |
| .pronunciation-word { |
| display: inline-block; |
| padding: 2px 4px; |
| margin: 0 1px; |
| border-radius: 4px; |
| transition: all 0.3s ease; |
| cursor: pointer; |
| font-family: 'Courier New', monospace; |
| letter-spacing: 1px; |
| color: #666; |
| background-color: transparent; |
| } |
| |
| .pronunciation-word:hover { |
| transform: translateY(-1px); |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
| } |
| |
| /* Active states when hovered/linked */ |
| .word-active { |
| background-color: rgba(110, 142, 251, 0.2) !important; |
| color: #6e8efb !important; |
| transform: translateY(-1px); |
| box-shadow: 0 2px 12px rgba(110, 142, 251, 0.3); |
| } |
| |
| .pronunciation-active { |
| background-color: rgba(110, 142, 251, 0.2) !important; |
| color: #6e8efb !important; |
| transform: translateY(-1px); |
| box-shadow: 0 2px 12px rgba(110, 142, 251, 0.3); |
| } |
| |
| /* Color classes - only used for subtle borders */ |
| .color-1 { border-left: 0px solid #6e8efb; } |
| .color-2 { border-left: 0px solid #a777e3; } |
| .color-3 { border-left: 0px solid #4facfe; } |
| .color-4 { border-left: 0px solid #00f2fe; } |
| .color-5 { border-left: 0px solid #43e97b; } |
| .color-6 { border-left: 0px solid #38f9d7; } |
| .color-7 { border-left: 0px solid #fa709a; } |
| .color-8 { border-left: 0px solid #b19709; } |
| |
| .pronunciation-separator { |
| color: #a777e3; |
| font-weight: bold; |
| margin: 0 2px; |
| } |
| |
| .analysis-card { |
| background: white; |
| padding: 1.5rem; |
| border-radius: 12px; |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| margin: 1rem 0; |
| } |
| |
| .section-title { |
| font-size: 0.9rem; |
| font-weight: 600; |
| color: #666; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| margin-bottom: 0.5rem; |
| } |
| |
| .results-text { |
| font-size: 1.1rem; |
| line-height: 1.8; |
| color: #666; |
| } |
| |
| .sample-buttons { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 0.5rem; |
| justify-content: center; |
| margin: 1rem 0; |
| } |
| |
| .sample-btn { |
| background: #f0f0f0; |
| border: none; |
| padding: 0.5rem 1rem; |
| border-radius: 20px; |
| cursor: pointer; |
| transition: background-color 0.2s; |
| } |
| |
| .sample-btn:hover { |
| background: #e0e0e0; |
| } |
| |
| .stats-container { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
| gap: 1rem; |
| margin: 1rem 0; |
| } |
| |
| .stat-box { |
| background: #f8f9fa; |
| padding: 1rem; |
| border-radius: 8px; |
| text-align: center; |
| } |
| |
| .stat-number { |
| font-size: 2rem; |
| font-weight: bold; |
| color: #6e8efb; |
| } |
| |
| .stat-label { |
| font-size: 0.9rem; |
| color: #666; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| |
| /* Interactive hover JavaScript */ |
| .hover-container { |
| position: relative; |
| } |
| </style> |
| |
| <script> |
| function addHoverInteraction() { |
| // Add event listeners for word-syllable linking |
| setTimeout(() => { |
| const words = document.querySelectorAll('.word-highlight'); |
| const pronunciations = document.querySelectorAll('.pronunciation-word'); |
| |
| // Clear any existing listeners |
| words.forEach(word => { |
| word.replaceWith(word.cloneNode(true)); |
| }); |
| pronunciations.forEach(pron => { |
| pron.replaceWith(pron.cloneNode(true)); |
| }); |
| |
| // Re-select after cloning |
| const newWords = document.querySelectorAll('.word-highlight'); |
| const newPronunciations = document.querySelectorAll('.pronunciation-word'); |
| |
| newWords.forEach((word, index) => { |
| word.addEventListener('mouseenter', () => { |
| // Highlight corresponding pronunciation |
| word.classList.add('word-active'); |
| if (newPronunciations[index]) { |
| newPronunciations[index].classList.add('pronunciation-active'); |
| } |
| }); |
| |
| word.addEventListener('mouseleave', () => { |
| // Remove highlights |
| word.classList.remove('word-active'); |
| if (newPronunciations[index]) { |
| newPronunciations[index].classList.remove('pronunciation-active'); |
| } |
| }); |
| }); |
| |
| newPronunciations.forEach((pron, index) => { |
| pron.addEventListener('mouseenter', () => { |
| // Highlight corresponding word |
| pron.classList.add('pronunciation-active'); |
| if (newWords[index]) { |
| newWords[index].classList.add('word-active'); |
| } |
| }); |
| |
| pron.addEventListener('mouseleave', () => { |
| // Remove highlights |
| pron.classList.remove('pronunciation-active'); |
| if (newWords[index]) { |
| newWords[index].classList.remove('word-active'); |
| } |
| }); |
| }); |
| }, 100); |
| } |
| |
| // Run the interaction setup when page loads |
| document.addEventListener('DOMContentLoaded', addHoverInteraction); |
| </script> |
| """, unsafe_allow_html=True) |
|
|
| class PronunciationAnalyzer: |
| def __init__(self): |
| self.dic = pyphen.Pyphen(lang='en') |
| self.color_classes = [ |
| 'color-1', 'color-2', 'color-3', 'color-4', |
| 'color-5', 'color-6', 'color-7', 'color-8' |
| ] |
| |
| def get_syllables(self, word: str) -> List[str]: |
| """Get syllables for a word using pyphen""" |
| |
| clean_word = word.lower().strip(string.punctuation) |
| |
| if not clean_word: |
| return [word] |
| |
| |
| syllables = self.dic.inserted(clean_word).split('-') |
| |
| |
| if len(syllables) == 1 and len(clean_word) > 3: |
| syllables = self._basic_syllable_split(clean_word) |
| |
| return syllables if syllables else [word] |
| |
| def _basic_syllable_split(self, word: str) -> List[str]: |
| """Basic vowel-based syllable splitting as fallback""" |
| vowels = 'aeiouy' |
| syllables = [] |
| current_syllable = '' |
| |
| for i, char in enumerate(word): |
| current_syllable += char |
| |
| |
| if i < len(word) - 1: |
| if char in vowels and word[i + 1] not in vowels: |
| |
| if len(current_syllable) >= 2: |
| syllables.append(current_syllable) |
| current_syllable = '' |
| |
| if current_syllable: |
| syllables.append(current_syllable) |
| |
| return syllables if syllables else [word] |
| |
| def tokenize_text(self, text: str) -> List[str]: |
| """Tokenize text into words while preserving punctuation""" |
| |
| |
| |
| |
| |
| |
| |
| |
| pattern = r"(?:\w+(?:'\w+)?|\d+|[^\w\s])" |
| tokens = re.findall(pattern, text) |
| |
| |
| result = [] |
| text_pos = 0 |
| |
| for token in tokens: |
| |
| token_pos = text.find(token, text_pos) |
| |
| |
| if token_pos > text_pos: |
| whitespace = text[text_pos:token_pos] |
| if whitespace.strip() == '': |
| result.extend(list(whitespace)) |
| |
| result.append(token) |
| text_pos = token_pos + len(token) |
| |
| |
| return [token for token in result if token.strip()] |
| |
| def analyze_text(self, text: str) -> Tuple[List[Tuple[str, List[str]]], dict]: |
| """Analyze text and return word-syllable pairs and statistics""" |
| if not text.strip(): |
| return [], {} |
| |
| |
| words = self.tokenize_text(text) |
| |
| |
| content_words = [word for word in words if any(c.isalnum() for c in word)] |
| |
| |
| word_syllables = [] |
| total_syllables = 0 |
| |
| for word in words: |
| if any(c.isalnum() for c in word): |
| syllables = self.get_syllables(word) |
| word_syllables.append((word, syllables)) |
| total_syllables += len(syllables) |
| else: |
| word_syllables.append((word, [word])) |
| |
| |
| stats = { |
| 'total_words': len(content_words), |
| 'total_syllables': total_syllables, |
| 'avg_syllables': round(total_syllables / len(content_words), 2) if content_words else 0, |
| 'longest_word': max(content_words, key=len) if content_words else '', |
| 'most_syllables': max(content_words, key=lambda w: len(self.get_syllables(w))) if content_words else '' |
| } |
| |
| return word_syllables, stats |
|
|
| def render_highlighted_text(word_syllables: List[Tuple[str, List[str]]], analyzer: PronunciationAnalyzer): |
| """Render original text with word highlighting""" |
| html_parts = [] |
| word_index = 0 |
| |
| for word, syllables in word_syllables: |
| if any(c.isalnum() for c in word): |
| color_class = analyzer.color_classes[word_index % len(analyzer.color_classes)] |
| html_parts.append(f'<span class="word-highlight {color_class}" data-word-index="{word_index}">{word}</span>') |
| word_index += 1 |
| else: |
| html_parts.append(word) |
| |
| |
| if word not in '.,!?;:': |
| html_parts.append(' ') |
| |
| return ''.join(html_parts) |
|
|
| def render_pronunciation(word_syllables: List[Tuple[str, List[str]]], analyzer: PronunciationAnalyzer): |
| """Render pronunciation with syllable breakdown""" |
| html_parts = [] |
| word_index = 0 |
| |
| for word, syllables in word_syllables: |
| if any(c.isalnum() for c in word): |
| color_class = analyzer.color_classes[word_index % len(analyzer.color_classes)] |
| |
| |
| syllable_text = '<span class="pronunciation-separator">ยท</span>'.join(syllables) |
| html_parts.append(f'<span class="pronunciation-word {color_class}" data-word-index="{word_index}">{syllable_text}</span>') |
| word_index += 1 |
| else: |
| html_parts.append(f'<span class="pronunciation-word">{word}</span>') |
| |
| |
| if word not in '.,!?;:': |
| html_parts.append(' ') |
| |
| return ''.join(html_parts) |
|
|
| def main(): |
| |
| analyzer = PronunciationAnalyzer() |
| |
| |
| st.markdown('<h1 class="main-header">๐ฃ๏ธ Text Pronunciation Analyzer</h1>', unsafe_allow_html=True) |
| st.markdown('<p class="subtitle">Enter any text below to see its pronunciation breakdown with advanced syllable detection</p>', unsafe_allow_html=True) |
| |
| |
| st.markdown("### ๐ Enter Your Text") |
| |
| |
| sample_texts = [ |
| "Hello world", |
| "Pronunciation analyzer", |
| "Supercalifragilisticexpialidocious", |
| "Linguistics and phonetics", |
| "The quick brown fox jumps over the lazy dog" |
| ] |
| |
| |
| st.markdown("**Try these samples:**") |
| cols = st.columns(len(sample_texts)) |
| for i, sample in enumerate(sample_texts): |
| if cols[i].button(sample, key=f"sample_{i}"): |
| st.session_state.input_text = sample |
| |
| |
| text_input = st.text_area( |
| "Text to analyze:", |
| value=st.session_state.get('input_text', ''), |
| height=120, |
| placeholder="Type or paste your text here...", |
| key="text_input" |
| ) |
| |
| |
| if text_input: |
| st.session_state.input_text = text_input |
| |
| |
| col1, col2, col3 = st.columns([1, 1, 1]) |
| with col2: |
| analyze_button = st.button("๐ Analyze Text", type="primary", use_container_width=True) |
| |
| |
| if st.button("๐๏ธ Clear"): |
| st.session_state.input_text = "" |
| st.rerun() |
| |
| |
| if analyze_button and text_input.strip(): |
| with st.spinner("Analyzing text..."): |
| word_syllables, stats = analyzer.analyze_text(text_input) |
| |
| if word_syllables: |
| st.markdown("---") |
| st.markdown("## ๐ Analysis Results") |
| |
| |
| st.markdown("### ๐ Text Statistics") |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.markdown(f""" |
| <div class="stat-box"> |
| <div class="stat-number">{stats['total_words']}</div> |
| <div class="stat-label">Words</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col2: |
| st.markdown(f""" |
| <div class="stat-box"> |
| <div class="stat-number">{stats['total_syllables']}</div> |
| <div class="stat-label">Syllables</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col3: |
| st.markdown(f""" |
| <div class="stat-box"> |
| <div class="stat-number">{stats['avg_syllables']}</div> |
| <div class="stat-label">Avg/Word</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col4: |
| longest_syllables = len(analyzer.get_syllables(stats['most_syllables'])) |
| st.markdown(f""" |
| <div class="stat-box"> |
| <div class="stat-number">{longest_syllables}</div> |
| <div class="stat-label">Max Syllables</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown("### ๐ Original Text") |
| original_html = render_highlighted_text(word_syllables, analyzer) |
| st.markdown(f'<div class="analysis-card hover-container"><div class="results-text">{original_html}</div></div>', unsafe_allow_html=True) |
| |
| |
| st.markdown("### ๐ค Pronunciation Breakdown") |
| pronunciation_html = render_pronunciation(word_syllables, analyzer) |
| st.markdown(f'<div class="analysis-card hover-container"><div class="results-text">{pronunciation_html}</div></div>', unsafe_allow_html=True) |
| |
| |
| st.components.v1.html(""" |
| <script> |
| function addHoverInteraction() { |
| const words = parent.document.querySelectorAll('.word-highlight'); |
| const pronunciations = parent.document.querySelectorAll('.pronunciation-word'); |
| |
| // Clear existing event listeners by cloning nodes |
| words.forEach((word, index) => { |
| const wordIndex = word.getAttribute('data-word-index'); |
| if (wordIndex !== null) { |
| const newWord = word.cloneNode(true); |
| word.parentNode.replaceChild(newWord, word); |
| |
| newWord.addEventListener('mouseenter', () => { |
| newWord.classList.add('word-active'); |
| const correspondingPron = parent.document.querySelector(`[data-word-index="${wordIndex}"].pronunciation-word`); |
| if (correspondingPron) { |
| correspondingPron.classList.add('pronunciation-active'); |
| } |
| }); |
| |
| newWord.addEventListener('mouseleave', () => { |
| newWord.classList.remove('word-active'); |
| const correspondingPron = parent.document.querySelector(`[data-word-index="${wordIndex}"].pronunciation-word`); |
| if (correspondingPron) { |
| correspondingPron.classList.remove('pronunciation-active'); |
| } |
| }); |
| } |
| }); |
| |
| pronunciations.forEach((pron, index) => { |
| const wordIndex = pron.getAttribute('data-word-index'); |
| if (wordIndex !== null) { |
| const newPron = pron.cloneNode(true); |
| pron.parentNode.replaceChild(newPron, pron); |
| |
| newPron.addEventListener('mouseenter', () => { |
| newPron.classList.add('pronunciation-active'); |
| const correspondingWord = parent.document.querySelector(`[data-word-index="${wordIndex}"].word-highlight`); |
| if (correspondingWord) { |
| correspondingWord.classList.add('word-active'); |
| } |
| }); |
| |
| newPron.addEventListener('mouseleave', () => { |
| newPron.classList.remove('pronunciation-active'); |
| const correspondingWord = parent.document.querySelector(`[data-word-index="${wordIndex}"].word-highlight`); |
| if (correspondingWord) { |
| correspondingWord.classList.remove('word-active'); |
| } |
| }); |
| } |
| }); |
| } |
| |
| // Run interaction setup |
| setTimeout(addHoverInteraction, 300); |
| </script> |
| """, height=0) |
| |
| |
| st.markdown("### ๐ Word-by-Word Analysis") |
| |
| |
| content_words = [(word, syllables) for word, syllables in word_syllables if any(c.isalnum() for c in word)] |
| |
| if content_words: |
| |
| for i in range(0, len(content_words), 3): |
| cols = st.columns(3) |
| for j, (word, syllables) in enumerate(content_words[i:i+3]): |
| with cols[j]: |
| st.markdown(f""" |
| <div style="background: #f8f9fa; padding: 1rem; border-radius: 8px; margin-bottom: 0.5rem;"> |
| <div style="font-weight: bold; color: #333; margin-bottom: 0.5rem;">{word}</div> |
| <div style="color: #666; font-family: monospace;">{'ยท'.join(syllables)}</div> |
| <div style="color: #999; font-size: 0.8rem;">{len(syllables)} syllable{'s' if len(syllables) != 1 else ''}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| elif analyze_button: |
| st.warning("Please enter some text to analyze.") |
| |
| |
| st.markdown("---") |
| st.markdown( |
| '<div style="text-align: center; color: #666; padding: 2rem;">Created by @aghilalb and ai</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| if __name__ == "__main__": |
| main() |