""" 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 "

Paste some text above to get started.

" # 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"""
Original Text
{escape_html(text)}
Version 1 — Spaced Syllables
Dashes between syllables. See the pieces.
{escape_html(v1)}
Version 2 — Sound It Out
Every word spelled how it sounds. Stressed syllables are LOUD.
{escape_html(v2_spaced)}
Version 3 — Slash Flow
Sounds connected with slashes. Read it like a river.
{escape_html(v3_slashed)}
Version 4 — "Professional" Phonetic (IPA)
This is what currently exists. The industry standard.
{escape_html(v4_ipa)}
Can you read it? Neither can they. This is what we're replacing.
""" return html def escape_html(text): """Basic HTML escaping.""" return text.replace("&", "&").replace("<", "<").replace(">", ">") # --- 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("""

Dyslexic Engine

Text should work for your brain. Not the other way around.

""") 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("""
Powered by NVIDIA Nemotron Nano Omni + 44 English phonemes
Heuremen — Build Small Hackathon 2026
""") if __name__ == "__main__": app.launch()