Update src/streamlit_app.py
Browse files- src/streamlit_app.py +282 -38
src/streamlit_app.py
CHANGED
|
@@ -1,40 +1,284 @@
|
|
| 1 |
-
import altair as alt
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
import syllables
|
| 3 |
+
import re
|
| 4 |
+
import random
|
| 5 |
+
from typing import List, Tuple
|
| 6 |
|
| 7 |
+
# Configure page
|
| 8 |
+
st.set_page_config(
|
| 9 |
+
page_title="Text Syllabification Tool",
|
| 10 |
+
page_icon="🔤",
|
| 11 |
+
layout="wide"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
def generate_color_palette(num_colors: int) -> List[str]:
|
| 15 |
+
"""Generate distinct colors for syllables"""
|
| 16 |
+
colors = [
|
| 17 |
+
"#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7",
|
| 18 |
+
"#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9",
|
| 19 |
+
"#F8C471", "#82E0AA", "#F1948A", "#85C1E9", "#D7BDE2",
|
| 20 |
+
"#A9DFBF", "#F9E79F", "#D5A6BD", "#AED6F1", "#ABEBC6"
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
# If we need more colors than predefined, generate random ones
|
| 24 |
+
while len(colors) < num_colors:
|
| 25 |
+
color = f"#{random.randint(0, 255):02x}{random.randint(0, 255):02x}{random.randint(0, 255):02x}"
|
| 26 |
+
colors.append(color)
|
| 27 |
+
|
| 28 |
+
return colors[:num_colors]
|
| 29 |
+
|
| 30 |
+
def syllabify_word(word: str) -> List[str]:
|
| 31 |
+
"""Syllabify a single word using the syllables library"""
|
| 32 |
+
# Clean the word of punctuation for syllabification
|
| 33 |
+
clean_word = re.sub(r'[^\w]', '', word)
|
| 34 |
+
|
| 35 |
+
if not clean_word:
|
| 36 |
+
return [word]
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
# Get syllable count
|
| 40 |
+
syl_count = syllables.estimate(clean_word)
|
| 41 |
+
|
| 42 |
+
# For basic syllabification, we'll use a simple approach
|
| 43 |
+
# The syllables library mainly provides counts, so we'll implement basic rules
|
| 44 |
+
syllable_list = simple_syllabify(clean_word)
|
| 45 |
+
|
| 46 |
+
# If our simple method doesn't match the count, adjust
|
| 47 |
+
if len(syllable_list) != syl_count and syl_count > 0:
|
| 48 |
+
syllable_list = adjust_syllables(clean_word, syl_count)
|
| 49 |
+
|
| 50 |
+
return syllable_list
|
| 51 |
+
except:
|
| 52 |
+
return [word]
|
| 53 |
+
|
| 54 |
+
def simple_syllabify(word: str) -> List[str]:
|
| 55 |
+
"""Simple syllabification based on vowel patterns"""
|
| 56 |
+
word = word.lower()
|
| 57 |
+
vowels = "aeiouy"
|
| 58 |
+
syllables_list = []
|
| 59 |
+
current_syllable = ""
|
| 60 |
+
|
| 61 |
+
i = 0
|
| 62 |
+
while i < len(word):
|
| 63 |
+
current_syllable += word[i]
|
| 64 |
+
|
| 65 |
+
# If current character is a vowel
|
| 66 |
+
if word[i] in vowels:
|
| 67 |
+
# Look ahead for consonant clusters
|
| 68 |
+
consonant_cluster = ""
|
| 69 |
+
j = i + 1
|
| 70 |
+
while j < len(word) and word[j] not in vowels:
|
| 71 |
+
consonant_cluster += word[j]
|
| 72 |
+
j += 1
|
| 73 |
+
|
| 74 |
+
# If we're at the end of the word, add everything
|
| 75 |
+
if j >= len(word):
|
| 76 |
+
current_syllable += consonant_cluster
|
| 77 |
+
syllables_list.append(current_syllable)
|
| 78 |
+
break
|
| 79 |
+
|
| 80 |
+
# Split consonant cluster
|
| 81 |
+
if len(consonant_cluster) <= 1:
|
| 82 |
+
# Single consonant goes with next syllable
|
| 83 |
+
syllables_list.append(current_syllable)
|
| 84 |
+
current_syllable = consonant_cluster
|
| 85 |
+
else:
|
| 86 |
+
# Multiple consonants: split them
|
| 87 |
+
split_point = len(consonant_cluster) // 2
|
| 88 |
+
current_syllable += consonant_cluster[:split_point]
|
| 89 |
+
syllables_list.append(current_syllable)
|
| 90 |
+
current_syllable = consonant_cluster[split_point:]
|
| 91 |
+
|
| 92 |
+
i = j - 1
|
| 93 |
+
|
| 94 |
+
i += 1
|
| 95 |
+
|
| 96 |
+
if current_syllable:
|
| 97 |
+
if syllables_list:
|
| 98 |
+
syllables_list[-1] += current_syllable
|
| 99 |
+
else:
|
| 100 |
+
syllables_list.append(current_syllable)
|
| 101 |
+
|
| 102 |
+
return syllables_list if syllables_list else [word]
|
| 103 |
+
|
| 104 |
+
def adjust_syllables(word: str, target_count: int) -> List[str]:
|
| 105 |
+
"""Adjust syllable split to match target count"""
|
| 106 |
+
simple_split = simple_syllabify(word)
|
| 107 |
+
|
| 108 |
+
if len(simple_split) == target_count:
|
| 109 |
+
return simple_split
|
| 110 |
+
|
| 111 |
+
# If we have fewer syllables than target, try to split more
|
| 112 |
+
if len(simple_split) < target_count:
|
| 113 |
+
# Split longest syllable
|
| 114 |
+
longest_idx = max(range(len(simple_split)), key=lambda i: len(simple_split[i]))
|
| 115 |
+
longest = simple_split[longest_idx]
|
| 116 |
+
|
| 117 |
+
if len(longest) > 2:
|
| 118 |
+
mid = len(longest) // 2
|
| 119 |
+
simple_split[longest_idx:longest_idx+1] = [longest[:mid], longest[mid:]]
|
| 120 |
+
|
| 121 |
+
# If we have more syllables than target, try to merge
|
| 122 |
+
elif len(simple_split) > target_count and len(simple_split) > 1:
|
| 123 |
+
# Merge shortest adjacent syllables
|
| 124 |
+
min_combined_len = float('inf')
|
| 125 |
+
merge_idx = 0
|
| 126 |
+
|
| 127 |
+
for i in range(len(simple_split) - 1):
|
| 128 |
+
combined_len = len(simple_split[i]) + len(simple_split[i + 1])
|
| 129 |
+
if combined_len < min_combined_len:
|
| 130 |
+
min_combined_len = combined_len
|
| 131 |
+
merge_idx = i
|
| 132 |
+
|
| 133 |
+
merged = simple_split[merge_idx] + simple_split[merge_idx + 1]
|
| 134 |
+
simple_split[merge_idx:merge_idx+2] = [merged]
|
| 135 |
+
|
| 136 |
+
return simple_split
|
| 137 |
+
|
| 138 |
+
def process_text(text: str) -> Tuple[List[Tuple[str, str]], List[Tuple[str, List[Tuple[str, str]]]]]:
|
| 139 |
+
"""Process text and return colored words and syllable breakdown"""
|
| 140 |
+
words = re.findall(r'\S+|\s+', text) # Keep spaces
|
| 141 |
+
|
| 142 |
+
colored_words = []
|
| 143 |
+
syllable_breakdown = []
|
| 144 |
+
|
| 145 |
+
for word in words:
|
| 146 |
+
if word.strip(): # If it's a word (not just whitespace)
|
| 147 |
+
syllables_list = syllabify_word(word.strip())
|
| 148 |
+
|
| 149 |
+
# Generate colors for this word's syllables
|
| 150 |
+
colors = generate_color_palette(len(syllables_list))
|
| 151 |
+
|
| 152 |
+
# For original text coloring, we need to map syllables back to original word
|
| 153 |
+
word_colored = []
|
| 154 |
+
syllable_colored = []
|
| 155 |
+
|
| 156 |
+
# Handle punctuation
|
| 157 |
+
clean_word = re.sub(r'[^\w]', '', word.strip())
|
| 158 |
+
prefix_punct = re.match(r'^[^\w]*', word.strip()).group()
|
| 159 |
+
suffix_punct = re.search(r'[^\w]*$', word.strip()).group()
|
| 160 |
+
|
| 161 |
+
if clean_word:
|
| 162 |
+
# Color each syllable
|
| 163 |
+
for i, syl in enumerate(syllables_list):
|
| 164 |
+
color = colors[i % len(colors)]
|
| 165 |
+
syllable_colored.append((syl, color))
|
| 166 |
+
|
| 167 |
+
# For the original word, we'll color it uniformly with the first syllable color
|
| 168 |
+
# or create a gradient effect
|
| 169 |
+
word_with_punct = prefix_punct + clean_word + suffix_punct
|
| 170 |
+
colored_words.append((word_with_punct, colors[0] if colors else "#000000"))
|
| 171 |
+
|
| 172 |
+
syllable_breakdown.append((word.strip(), syllable_colored))
|
| 173 |
+
else:
|
| 174 |
+
colored_words.append((word, "#000000"))
|
| 175 |
+
else:
|
| 176 |
+
colored_words.append((word, "#000000")) # Spaces remain uncolored
|
| 177 |
+
|
| 178 |
+
return colored_words, syllable_breakdown
|
| 179 |
+
|
| 180 |
+
def display_colored_text(colored_words: List[Tuple[str, str]]):
|
| 181 |
+
"""Display text with colored words"""
|
| 182 |
+
html_parts = []
|
| 183 |
+
for word, color in colored_words:
|
| 184 |
+
if word.strip():
|
| 185 |
+
html_parts.append(f'<span style="color: {color}; font-weight: bold;">{word}</span>')
|
| 186 |
+
else:
|
| 187 |
+
html_parts.append(word)
|
| 188 |
+
|
| 189 |
+
html_content = ''.join(html_parts)
|
| 190 |
+
st.markdown(f'<div style="font-size: 18px; line-height: 1.6; padding: 15px; border: 2px solid #ddd; border-radius: 10px; background-color: #f9f9f9;">{html_content}</div>', unsafe_allow_html=True)
|
| 191 |
+
|
| 192 |
+
def display_syllable_breakdown(syllable_breakdown: List[Tuple[str, List[Tuple[str, str]]]]):
|
| 193 |
+
"""Display syllable breakdown"""
|
| 194 |
+
html_parts = []
|
| 195 |
+
|
| 196 |
+
for word, syllables in syllable_breakdown:
|
| 197 |
+
if syllables:
|
| 198 |
+
word_html = []
|
| 199 |
+
for syl, color in syllables:
|
| 200 |
+
word_html.append(f'<span style="color: {color}; font-weight: bold; margin-right: 2px;">{syl}</span>')
|
| 201 |
+
|
| 202 |
+
# Add separator between syllables
|
| 203 |
+
syllable_display = '<span style="color: #666;">|</span>'.join([
|
| 204 |
+
f'<span style="color: {color}; font-weight: bold;">{syl}</span>'
|
| 205 |
+
for syl, color in syllables
|
| 206 |
+
])
|
| 207 |
+
|
| 208 |
+
html_parts.append(f'<div style="margin-bottom: 10px;"><strong>{word}:</strong> {syllable_display}</div>')
|
| 209 |
+
|
| 210 |
+
if html_parts:
|
| 211 |
+
html_content = ''.join(html_parts)
|
| 212 |
+
st.markdown(f'<div style="font-size: 16px; line-height: 1.8; padding: 15px; border: 2px solid #ddd; border-radius: 10px; background-color: #f0f8ff;">{html_content}</div>', unsafe_allow_html=True)
|
| 213 |
+
|
| 214 |
+
# Main app
|
| 215 |
+
def main():
|
| 216 |
+
st.title("🔤 Text Syllabification Tool")
|
| 217 |
+
st.markdown("Enter text below to see syllable breakdown with color coding!")
|
| 218 |
+
|
| 219 |
+
# Input text area
|
| 220 |
+
input_text = st.text_area(
|
| 221 |
+
"Enter your text:",
|
| 222 |
+
placeholder="Type or paste your text here...",
|
| 223 |
+
height=150,
|
| 224 |
+
help="Enter any text to see how words are broken down into syllables"
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
if input_text.strip():
|
| 228 |
+
# Process the text
|
| 229 |
+
colored_words, syllable_breakdown = process_text(input_text)
|
| 230 |
+
|
| 231 |
+
# Create two columns
|
| 232 |
+
col1, col2 = st.columns(2)
|
| 233 |
+
|
| 234 |
+
with col1:
|
| 235 |
+
st.subheader("Original Text (Colored by First Syllable)")
|
| 236 |
+
display_colored_text(colored_words)
|
| 237 |
+
|
| 238 |
+
with col2:
|
| 239 |
+
st.subheader("Syllable Breakdown")
|
| 240 |
+
display_syllable_breakdown(syllable_breakdown)
|
| 241 |
+
|
| 242 |
+
# Statistics
|
| 243 |
+
total_words = len([word for word, _ in syllable_breakdown])
|
| 244 |
+
total_syllables = sum(len(syls) for _, syls in syllable_breakdown)
|
| 245 |
+
|
| 246 |
+
st.markdown("---")
|
| 247 |
+
st.markdown(f"**Statistics:** {total_words} words, {total_syllables} syllables")
|
| 248 |
+
|
| 249 |
+
# Show syllable counts
|
| 250 |
+
if syllable_breakdown:
|
| 251 |
+
st.subheader("Syllable Counts")
|
| 252 |
+
counts_html = []
|
| 253 |
+
for word, syllables in syllable_breakdown:
|
| 254 |
+
count = len(syllables)
|
| 255 |
+
counts_html.append(f"<span style='margin-right: 15px;'><strong>{word}</strong>: {count}</span>")
|
| 256 |
+
|
| 257 |
+
counts_display = " ".join(counts_html)
|
| 258 |
+
st.markdown(f'<div style="padding: 10px; background-color: #f0f0f0; border-radius: 5px;">{counts_display}</div>', unsafe_allow_html=True)
|
| 259 |
+
|
| 260 |
+
else:
|
| 261 |
+
st.info("👆 Enter some text above to see the syllabification in action!")
|
| 262 |
+
|
| 263 |
+
# Example
|
| 264 |
+
st.subheader("Example")
|
| 265 |
+
example_text = "Beautiful mountains and valleys create wonderful landscapes."
|
| 266 |
+
st.code(example_text)
|
| 267 |
+
|
| 268 |
+
if st.button("Try Example"):
|
| 269 |
+
st.session_state.example_clicked = True
|
| 270 |
+
# Process example
|
| 271 |
+
colored_words, syllable_breakdown = process_text(example_text)
|
| 272 |
+
|
| 273 |
+
col1, col2 = st.columns(2)
|
| 274 |
+
|
| 275 |
+
with col1:
|
| 276 |
+
st.subheader("Original Text (Colored)")
|
| 277 |
+
display_colored_text(colored_words)
|
| 278 |
+
|
| 279 |
+
with col2:
|
| 280 |
+
st.subheader("Syllable Breakdown")
|
| 281 |
+
display_syllable_breakdown(syllable_breakdown)
|
| 282 |
+
|
| 283 |
+
if __name__ == "__main__":
|
| 284 |
+
main()
|