Download src/streamlit_app.py from aghilTQ/Syllable: direct link, hf CLI and curl.
- Browser
- Download file 21.8 kB
-
https://huggingface.co/spaces/aghilTQ/Syllable/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/aghilTQ/Syllable/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/aghilTQ/Syllable/resolve/main/src/streamlit_app.py
21.8 kB
| import streamlit as st | |
| import pyphen | |
| import re | |
| from typing import List, Tuple | |
| import string | |
| import os | |
| # Initialize pyphen for syllable splitting | |
| dic = pyphen.Pyphen(lang='en') | |
| # Configure Streamlit page | |
| st.set_page_config( | |
| page_title="Text Pronunciation Analyzer", | |
| page_icon="π£οΈ", | |
| layout="wide", | |
| initial_sidebar_state="collapsed" | |
| ) | |
| # Custom CSS for styling | |
| 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""" | |
| # Remove punctuation and convert to lowercase | |
| clean_word = word.lower().strip(string.punctuation) | |
| if not clean_word: | |
| return [word] | |
| # Use pyphen to split into syllables | |
| syllables = self.dic.inserted(clean_word).split('-') | |
| # If pyphen couldn't split (returns original word), try basic vowel-based splitting | |
| 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 | |
| # Look ahead for vowel patterns | |
| if i < len(word) - 1: | |
| if char in vowels and word[i + 1] not in vowels: | |
| # Vowel followed by consonant - potential syllable break | |
| 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""" | |
| # Enhanced regex-based tokenization | |
| # This pattern matches: | |
| # - Words (including contractions like "don't") | |
| # - Numbers | |
| # - Punctuation marks | |
| # - Preserves spacing | |
| # Split text into tokens while preserving structure | |
| pattern = r"(?:\w+(?:'\w+)?|\d+|[^\w\s])" | |
| tokens = re.findall(pattern, text) | |
| # Add spaces back where needed | |
| result = [] | |
| text_pos = 0 | |
| for token in tokens: | |
| # Find the token's position in the original text | |
| token_pos = text.find(token, text_pos) | |
| # Add any whitespace before the token | |
| if token_pos > text_pos: | |
| whitespace = text[text_pos:token_pos] | |
| if whitespace.strip() == '': # Only add if it's pure whitespace | |
| result.extend(list(whitespace)) | |
| result.append(token) | |
| text_pos = token_pos + len(token) | |
| # Filter out empty strings and normalize | |
| 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 [], {} | |
| # Tokenize the text | |
| words = self.tokenize_text(text) | |
| # Filter out pure punctuation tokens for analysis | |
| content_words = [word for word in words if any(c.isalnum() for c in word)] | |
| # Get syllables for each word | |
| word_syllables = [] | |
| total_syllables = 0 | |
| for word in words: | |
| if any(c.isalnum() for c in word): # Only analyze words with alphanumeric characters | |
| syllables = self.get_syllables(word) | |
| word_syllables.append((word, syllables)) | |
| total_syllables += len(syllables) | |
| else: | |
| word_syllables.append((word, [word])) # Keep punctuation as-is | |
| # Calculate statistics | |
| 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) | |
| # Add space after word (except for punctuation that shouldn't have spaces) | |
| 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)] | |
| # Join syllables with dots | |
| 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>') | |
| # Add space after word (except for punctuation that shouldn't have spaces) | |
| if word not in '.,!?;:': | |
| html_parts.append(' ') | |
| return ''.join(html_parts) | |
| def main(): | |
| # Initialize analyzer | |
| analyzer = PronunciationAnalyzer() | |
| # Header | |
| 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) | |
| # Input section | |
| st.markdown("### π Enter Your Text") | |
| # Sample texts | |
| sample_texts = [ | |
| "Hello world", | |
| "Pronunciation analyzer", | |
| "Supercalifragilisticexpialidocious", | |
| "Linguistics and phonetics", | |
| "The quick brown fox jumps over the lazy dog" | |
| ] | |
| # Sample buttons | |
| 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 | |
| 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" | |
| ) | |
| # Update session state | |
| if text_input: | |
| st.session_state.input_text = text_input | |
| # Analyze button | |
| col1, col2, col3 = st.columns([1, 1, 1]) | |
| with col2: | |
| analyze_button = st.button("π Analyze Text", type="primary", use_container_width=True) | |
| # Clear button | |
| if st.button("ποΈ Clear"): | |
| st.session_state.input_text = "" | |
| st.rerun() | |
| # Analysis results | |
| 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") | |
| # Statistics | |
| 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) | |
| # Original text with highlights | |
| 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) | |
| # Pronunciation breakdown | |
| 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) | |
| # Add JavaScript for hover interaction | |
| 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) | |
| # Word-by-word breakdown | |
| st.markdown("### π Word-by-Word Analysis") | |
| # Create expandable sections for detailed breakdown | |
| content_words = [(word, syllables) for word, syllables in word_syllables if any(c.isalnum() for c in word)] | |
| if content_words: | |
| # Group words into rows of 3 | |
| 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.") | |
| # Footer | |
| 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() |