dyslexic-engine / app.py
Wayfinder6's picture
Upload folder using huggingface_hub
e96bf82 verified
Raw
History Blame Contribute Delete
13.3 kB
"""
Dyslexic Engine — Text should work for YOUR brain.
Built for the Build Small Hackathon 2026.
Shows the original text, then three dyslexic-friendly versions beneath:
1. Spaced Syllables — dashes between syllables
2. Sound It Out — simple phonemes with spaces
3. Slash Flow — phonemes with slashes
Runs entirely local. No cloud APIs. No data leaves your machine.
"""
import gradio as gr
import pyphen
import os
import re
from openai import OpenAI
from phonemes import text_to_ipa
# --- NVIDIA Nemotron ---
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")
NEMOTRON_MODEL = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
nvidia_client = None
if NVIDIA_API_KEY:
nvidia_client = OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=NVIDIA_API_KEY,
)
print("NVIDIA Nemotron connected.")
else:
print("No NVIDIA_API_KEY — running in fallback mode.")
# --- Syllable Engine ---
dic = pyphen.Pyphen(lang="en_US")
def get_word_parts(word):
"""Extract prefix punctuation, core word, suffix punctuation."""
prefix = ""
suffix = ""
core = word
while core and not core[0].isalnum():
prefix += core[0]
core = core[1:]
while core and not core[-1].isalnum():
suffix = core[-1] + suffix
core = core[:-1]
return prefix, core, suffix
def split_syllables(word):
"""Split a word into syllables using pyphen."""
clean = re.sub(r'[^\w]', '', word)
if not clean:
return [word]
hyphenated = dic.inserted(clean)
parts = hyphenated.split("-")
return parts if parts else [word]
# --- VERSION 1: Spaced Syllables ---
def spaced_syllables(text):
"""Each word broken into syllables with ' - ' between them."""
words = text.split()
result = []
for word in words:
prefix, core, suffix = get_word_parts(word)
if not core:
result.append(word)
continue
syls = split_syllables(core)
if len(syls) > 1:
spaced = " - ".join(syls)
else:
spaced = syls[0]
result.append(f"{prefix}{spaced}{suffix}")
return " ".join(result)
# --- VERSION 2 & 3: Phoneme versions (LLM-generated) ---
def generate_phonemes(text):
"""Use the LLM to generate simple phonetic pronunciations for every word.
Returns two versions: spaced and slashed."""
if nvidia_client:
prompt = f"""Convert every word in this paragraph to simple phonetic pronunciation.
Rules:
- Use simple everyday letter combinations, NOT IPA symbols
- Capitalize the stressed syllable in multi-syllable words
- Keep the same word order as the original
- Separate sounds within a word with spaces
- Short words (a, the, is, by) still get converted
- Output ONLY the phonetic version, nothing else
Example:
Input: "The cat sat on the mat"
Output: "thuh kat sat on thuh mat"
Input: "Photosynthesis is the process"
Output: "foh toh SIN thuh sis iz thuh PRAH sess"
Now convert this text:
{text[:600]}"""
try:
response = nvidia_client.chat.completions.create(
model=NEMOTRON_MODEL,
messages=[
{"role": "system", "content": "You convert text to simple phonetic pronunciation. Output ONLY the pronunciation, keeping the same word order. Use everyday letter combinations, not IPA. Capitalize stressed syllables. Separate sounds within words with spaces."},
{"role": "user", "content": prompt}
],
max_tokens=800,
temperature=0.2,
)
spaced_phonemes = response.choices[0].message.content.strip()
slash_phonemes = convert_spaced_to_slashed(spaced_phonemes)
return spaced_phonemes, slash_phonemes
except Exception as e:
fallback = _rule_based_phonemes(text)
return fallback, fallback.replace(" ", " ").replace(" ", "/").replace("//" , " ")
else:
fallback = _rule_based_phonemes(text)
return fallback, fallback
def convert_spaced_to_slashed(spaced_text):
"""Convert spaced phoneme text to slash-separated version.
Words are separated by double spaces, sounds within words by single spaces."""
# Split on double-space (word boundaries)
word_chunks = re.split(r' +', spaced_text)
slashed = []
for chunk in word_chunks:
chunk = chunk.strip()
if not chunk:
continue
# If the chunk has spaces, those are sound boundaries within a word
sounds = chunk.split()
if len(sounds) > 1:
slashed.append("/".join(sounds))
else:
slashed.append(chunk)
return " ".join(slashed)
def _rule_based_phonemes(text):
"""Fallback: just use syllable splitting as a rough phoneme guide."""
words = text.split()
result = []
for word in words:
prefix, core, suffix = get_word_parts(word)
if not core:
result.append(word)
continue
syls = split_syllables(core)
result.append(f"{prefix}{' '.join(syls).lower()}{suffix}")
return " ".join(result)
# --- VERSION 4: IPA "Professional" Phonetic (the punchline) ---
def generate_ipa(text):
"""Generate full IPA transcription from the 44 phonemes mapping.
Deliberately unreadable. That's the entire point.
Uses our phoneme engine — no LLM needed, instant, and accurate enough
to look terrifyingly professional."""
return text_to_ipa(text)
# --- Main Transform ---
def transform_text(text):
"""Generate all three versions from input text."""
if not text or not text.strip():
return "<p style='color:#999; font-family: OpenDyslexic, sans-serif;'>Paste some text above to get started.</p>"
# Version 1: Spaced syllables (rule-based, instant)
v1 = spaced_syllables(text)
# Version 2 & 3: Phoneme versions (LLM)
v2_spaced, v3_slashed = generate_phonemes(text)
# Version 4: IPA — the "professional" version nobody can read
v4_ipa = generate_ipa(text)
html = f"""
<div class="de-container">
<div class="de-section de-original">
<div class="de-label">Original Text</div>
<div class="de-text de-standard-font">{escape_html(text)}</div>
</div>
<div class="de-divider"></div>
<div class="de-section de-version de-v1">
<div class="de-label">Version 1 — Spaced Syllables</div>
<div class="de-sublabel">Dashes between syllables. See the pieces.</div>
<div class="de-text de-dyslexic">{escape_html(v1)}</div>
</div>
<div class="de-section de-version de-v2">
<div class="de-label">Version 2 — Sound It Out</div>
<div class="de-sublabel">Every word spelled how it sounds. Stressed syllables are LOUD.</div>
<div class="de-text de-dyslexic de-phoneme">{escape_html(v2_spaced)}</div>
</div>
<div class="de-section de-version de-v3">
<div class="de-label">Version 3 — Slash Flow</div>
<div class="de-sublabel">Sounds connected with slashes. Read it like a river.</div>
<div class="de-text de-dyslexic de-phoneme">{escape_html(v3_slashed)}</div>
</div>
<div class="de-divider"></div>
<div class="de-section de-version de-v4">
<div class="de-label">Version 4 — "Professional" Phonetic (IPA)</div>
<div class="de-sublabel">This is what currently exists. The industry standard.</div>
<div class="de-text de-ipa">{escape_html(v4_ipa)}</div>
<div class="de-ipa-caption">Can you read it?&ensp;Neither can they.&ensp;This is what we're replacing.</div>
</div>
<div class="de-footer">
Three versions that work for your brain. One that doesn't work for anyone's.<br>
Pick the one that clicks. There's no wrong answer.
</div>
</div>
"""
return html
def escape_html(text):
"""Basic HTML escaping."""
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# --- Custom CSS ---
CUSTOM_CSS = """
@import url('https://fonts.cdnfonts.com/css/opendyslexic');
/* Container */
.de-container {
background: #FFF8F0;
border-radius: 16px;
padding: 32px;
max-width: 800px;
margin: 0 auto;
}
/* Original text section */
.de-original {
background: #FFFFFF;
border: 1px solid #E0D8CC;
border-radius: 8px;
padding: 20px;
}
.de-standard-font {
font-family: Georgia, 'Times New Roman', serif !important;
font-size: 1.05em;
line-height: 1.6;
color: #333;
}
/* Dyslexic versions */
.de-dyslexic {
font-family: 'OpenDyslexic', sans-serif !important;
font-size: 1.25em;
line-height: 2.4;
letter-spacing: 0.1em;
word-spacing: 0.3em;
color: #2C2C2C;
}
.de-phoneme {
color: #4A6741;
}
/* Section styling */
.de-section {
margin-bottom: 20px;
}
.de-version {
background: #FFFDF8;
border-left: 4px solid #E07A5F;
border-radius: 0 8px 8px 0;
padding: 16px 20px;
}
.de-version:nth-child(4) {
border-left-color: #3D85C6;
}
.de-version:nth-child(5) {
border-left-color: #81B29A;
}
.de-label {
font-family: 'OpenDyslexic', sans-serif;
font-size: 0.85em;
font-weight: 700;
color: #8B7355;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 4px;
}
.de-sublabel {
font-family: 'OpenDyslexic', sans-serif;
font-size: 0.7em;
color: #B8A88A;
margin-bottom: 12px;
font-style: italic;
}
.de-text {
padding: 8px 0;
}
.de-divider {
height: 2px;
background: linear-gradient(to right, #E07A5F, #3D85C6, #81B29A);
margin: 24px 0;
border-radius: 2px;
opacity: 0.4;
}
/* Version 4 — the punchline */
.de-v4 {
background: #F0EDED !important;
border-left: 4px solid #999 !important;
opacity: 0.75;
}
.de-ipa {
font-family: 'Courier New', monospace !important;
font-size: 1.1em;
line-height: 2.0;
color: #666;
letter-spacing: 0.05em;
}
.de-ipa-caption {
font-family: 'OpenDyslexic', sans-serif;
font-size: 0.8em;
color: #E07A5F;
margin-top: 12px;
font-weight: 700;
letter-spacing: 0.05em;
}
.de-footer {
text-align: center;
font-family: 'OpenDyslexic', sans-serif;
font-size: 0.8em;
color: #B8A88A;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #E8E0D4;
font-style: italic;
line-height: 1.8;
}
/* Global */
.gradio-container {
background: #FEFCF8 !important;
max-width: 900px !important;
margin: 0 auto !important;
}
.gr-button-primary {
background: #E07A5F !important;
border: none !important;
font-family: 'OpenDyslexic', sans-serif !important;
font-size: 1.1em !important;
padding: 14px 40px !important;
border-radius: 8px !important;
}
.gr-button-primary:hover {
background: #C96A52 !important;
}
footer {
display: none !important;
}
"""
# --- Examples ---
EXAMPLES = [
["Photosynthesis is the process by which green plants and some other organisms use sunlight to synthesize foods from carbon dioxide and water."],
["The mitochondria is the powerhouse of the cell. It produces most of the cell's supply of adenosine triphosphate, used as a source of chemical energy."],
["When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them with another."],
["The boy couldn't understand why the letters kept moving. Every night at the kitchen table, the homework looked like a foreign language. His dad sat next to him, trying not to show how helpless he felt."],
]
# --- Gradio App ---
with gr.Blocks(css=CUSTOM_CSS, title="Dyslexic Engine", theme=gr.themes.Soft()) as app:
gr.HTML("""
<div style="text-align:center; padding: 24px 0 16px;">
<h1 style="font-family: 'OpenDyslexic', Georgia, serif; color: #2C2C2C; font-size: 2.4em; font-weight: normal; letter-spacing: 0.03em;">
Dyslexic Engine
</h1>
<p style="font-family: 'OpenDyslexic', Georgia, serif; color: #8B7355; font-size: 1.05em; letter-spacing: 0.05em; line-height: 1.8;">
Text should work for <em>your</em> brain. Not the other way around.
</p>
</div>
""")
text_input = gr.Textbox(
label="Paste any text",
placeholder="Paste homework, an article, an email — anything that's hard to read...",
lines=6,
max_lines=15,
)
transform_btn = gr.Button("Transform", variant="primary", size="lg")
output_html = gr.HTML()
transform_btn.click(
fn=transform_text,
inputs=[text_input],
outputs=[output_html],
)
gr.Examples(
examples=EXAMPLES,
inputs=[text_input],
label="Try these",
)
gr.HTML("""
<div style="text-align:center; padding: 20px 0 8px; color: #BBA88A; font-size: 0.7em; letter-spacing: 0.1em; font-family: 'OpenDyslexic', sans-serif;">
Powered by NVIDIA Nemotron Nano Omni + 44 English phonemes<br>
<span style="color:#D4A574;">Heuremen — Build Small Hackathon 2026</span>
</div>
""")
if __name__ == "__main__":
app.launch()