| """ |
| 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_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.") |
|
|
| |
| 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] |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| 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.""" |
| |
| word_chunks = re.split(r' +', spaced_text) |
| slashed = [] |
| for chunk in word_chunks: |
| chunk = chunk.strip() |
| if not chunk: |
| continue |
| |
| 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) |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| 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>" |
|
|
| |
| v1 = spaced_syllables(text) |
|
|
| |
| v2_spaced, v3_slashed = generate_phonemes(text) |
|
|
| |
| 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? Neither can they. 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("&", "&").replace("<", "<").replace(">", ">") |
|
|
|
|
| |
| 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 = [ |
| ["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."], |
| ] |
|
|
| |
| 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() |
|
|