TTS/STT can't tell "wind" from "wind" — how do you handle heteronyms in a pronunciation-teaching app?

I'm building a vocabulary-learning app in Flutter where hearing and saying the word correctly is the product, not a nice-to-have. I've hit a problem I can't design around and I'd rather ask than keep patching.

The stack

  • Flutter, ~1,600 words live across EN/ES/PT/IT/FR
  • TTS: ElevenLabs (eleven_multilingual_v2) called through a Supabase Edge Function so the key never ships in the client
  • Every clip cached server-side once per (text, language), shared across all users — so a given string is synthesized exactly once, ever
  • Cached again on-device (150MB LRU) so replays are instant and offline
  • flutter_tts as fallback behind a 2.5s timeout so playback never goes silent
  • STT: speech_to_text for a pronunciation-practice screen — hear the word, say it, get graded

The problem: heteronyms, in both directions

Output. "Wind" (moving air) and "wind" (to coil) are the same string and different sounds. TTS picks one reading and commits. My word library actually knows which sense is on screen — every entry carries a part of speech — but there's no API surface to hand that over. ElevenLabs pronunciation dictionaries are exact-string, case-sensitive, and have no POS or context scoping, so one spelling gets one entry and the second sense is unreachable. Phoneme tags do exist, but per the docs only on eleven_flash_v2 and v3 — not the multilingual model I'm on, and switching models means re-synthesizing the whole cache and losing voice identity across five languages.

Input. This is the part that actually bothers me. The practice screen normalizes the transcript and Levenshtein-scores it against the target. But STT returns orthography — say either reading of "wind" and the transcript is "wind" either way. A learner who mispronounces it scores full marks. The feature is structurally incapable of catching the error it exists to catch.

What I've tried

Respelling the audio-only string before it reaches the engine — the screen text is never touched. wind(noun) → winned, wind(verb) → wined, read(past) → red, and so on. This is basically ElevenLabs' own recommended "alias" workaround and it works for the ~8 vowel-shift pairs I've mapped. Side benefit: since my cache key is a hash of (lang + text), two senses naturally get two cache entries.

It fails in three ways:

  1. Stress-shift pairs. REcord/reCORD, PREsent/preSENT, CONtent/conTENT. Respelling can't encode stress, and I haven't found a trick spelling that does.
  2. Monolingual. It's an English orthography hack. Nothing about it transfers to ES/PT/IT/FR, all of which have their own homographs.
  3. Manual. Hand-curated table. Doesn't scale to a few thousand words.

What I'm actually asking

  1. Is there a TTS API that accepts a sense/POS hint, or per-request phonemes, on a multilingual model? Or does everyone route heteronyms to a separate English-only model and eat the voice mismatch?
  2. If IPA is the only real answer — has anyone found v3-class IPA reliable enough in production? The docs quote 80–90% consistency, which for a teaching app means the wrong pronunciation ships to a learner one time in eight.
  3. For stress-shift specifically: any orthographic trick that works, or is phoneme-level control genuinely the only path?
  4. On the STT side — is there a mobile-viable way to get phonemes rather than words? I've looked at wav2vec2 phoneme-CTC or a forced aligner with GOP scoring via ONNX on-device, but I don't know if that's realistic on a mid-range phone or if I'm about to spend a month learning that it isn't. Whisper doesn't help; it also returns orthography.
  5. The unglamorous option: detect heteronyms and simply disable pronunciation scoring for them, with an honest note to the user. Is that what shipped apps actually do?

If you've built pronunciation feedback into anything real, I'd love to know where you drew the line between "graded properly" and "good enough." Happy to share code for any of the above.

reddit.com
u/Fair_Expression_3291 — 3 days ago

TTS/STT can't tell "wind" from "wind" — how do you handle heteronyms in a pronunciation-teaching app?

I'm building a vocabulary-learning app in Flutter where hearing and saying the word correctly is the product, not a nice-to-have. I've hit a problem I can't design around and I'd rather ask than keep patching.

The stack

  • Flutter, ~1,600 words live across EN/ES/PT/IT/FR
  • TTS: ElevenLabs (eleven_multilingual_v2) called through a Supabase Edge Function so the key never ships in the client
  • Every clip cached server-side once per (text, language), shared across all users — so a given string is synthesized exactly once, ever
  • Cached again on-device (150MB LRU) so replays are instant and offline
  • flutter_tts as fallback behind a 2.5s timeout so playback never goes silent
  • STT: speech_to_text for a pronunciation-practice screen — hear the word, say it, get graded

The problem: heteronyms, in both directions

Output. "Wind" (moving air) and "wind" (to coil) are the same string and different sounds. TTS picks one reading and commits. My word library actually knows which sense is on screen — every entry carries a part of speech — but there's no API surface to hand that over. ElevenLabs pronunciation dictionaries are exact-string, case-sensitive, and have no POS or context scoping, so one spelling gets one entry and the second sense is unreachable. Phoneme tags do exist, but per the docs only on eleven_flash_v2 and v3 — not the multilingual model I'm on, and switching models means re-synthesizing the whole cache and losing voice identity across five languages.

Input. This is the part that actually bothers me. The practice screen normalizes the transcript and Levenshtein-scores it against the target. But STT returns orthography — say either reading of "wind" and the transcript is "wind" either way. A learner who mispronounces it scores full marks. The feature is structurally incapable of catching the error it exists to catch.

What I've tried

Respelling the audio-only string before it reaches the engine — the screen text is never touched. wind(noun) → winned, wind(verb) → wined, read(past) → red, and so on. This is basically ElevenLabs' own recommended "alias" workaround and it works for the ~8 vowel-shift pairs I've mapped. Side benefit: since my cache key is a hash of (lang + text), two senses naturally get two cache entries.

It fails in three ways:

  1. Stress-shift pairs. REcord/reCORD, PREsent/preSENT, CONtent/conTENT. Respelling can't encode stress, and I haven't found a trick spelling that does.
  2. Monolingual. It's an English orthography hack. Nothing about it transfers to ES/PT/IT/FR, all of which have their own homographs.
  3. Manual. Hand-curated table. Doesn't scale to a few thousand words.

What I'm actually asking

  1. Is there a TTS API that accepts a sense/POS hint, or per-request phonemes, on a multilingual model? Or does everyone route heteronyms to a separate English-only model and eat the voice mismatch?
  2. If IPA is the only real answer — has anyone found v3-class IPA reliable enough in production? The docs quote 80–90% consistency, which for a teaching app means the wrong pronunciation ships to a learner one time in eight.
  3. For stress-shift specifically: any orthographic trick that works, or is phoneme-level control genuinely the only path?
  4. On the STT side — is there a mobile-viable way to get phonemes rather than words? I've looked at wav2vec2 phoneme-CTC or a forced aligner with GOP scoring via ONNX on-device, but I don't know if that's realistic on a mid-range phone or if I'm about to spend a month learning that it isn't. Whisper doesn't help; it also returns orthography.
  5. The unglamorous option: detect heteronyms and simply disable pronunciation scoring for them, with an honest note to the user. Is that what shipped apps actually do?

If you've built pronunciation feedback into anything real, I'd love to know where you drew the line between "graded properly" and "good enough." Happy to share code for any of the above.

reddit.com
u/Fair_Expression_3291 — 3 days ago
▲ 6 r/ElevenLabs+1 crossposts

TTS/STT can't tell "wind" from "wind" — how do you handle heteronyms in a pronunciation-teaching app?

I'm building a vocabulary-learning app in Flutter where hearing and saying the word correctly is the product, not a nice-to-have. I've hit a problem I can't design around and I'd rather ask than keep patching.

The stack

  • Flutter, ~1,600 words live across EN/ES/PT/IT/FR
  • TTS: ElevenLabs (eleven_multilingual_v2) called through a Supabase Edge Function so the key never ships in the client
  • Every clip cached server-side once per (text, language), shared across all users — so a given string is synthesized exactly once, ever
  • Cached again on-device (150MB LRU) so replays are instant and offline
  • flutter_tts as fallback behind a 2.5s timeout so playback never goes silent
  • STT: speech_to_text for a pronunciation-practice screen — hear the word, say it, get graded

The problem: heteronyms, in both directions

Output. "Wind" (moving air) and "wind" (to coil) are the same string and different sounds. TTS picks one reading and commits. My word library actually knows which sense is on screen — every entry carries a part of speech — but there's no API surface to hand that over. ElevenLabs pronunciation dictionaries are exact-string, case-sensitive, and have no POS or context scoping, so one spelling gets one entry and the second sense is unreachable. Phoneme tags do exist, but per the docs only on eleven_flash_v2 and v3 — not the multilingual model I'm on, and switching models means re-synthesizing the whole cache and losing voice identity across five languages.

Input. This is the part that actually bothers me. The practice screen normalizes the transcript and Levenshtein-scores it against the target. But STT returns orthography — say either reading of "wind" and the transcript is "wind" either way. A learner who mispronounces it scores full marks. The feature is structurally incapable of catching the error it exists to catch.

What I've tried

Respelling the audio-only string before it reaches the engine — the screen text is never touched. wind(noun) → winned, wind(verb) → wined, read(past) → red, and so on. This is basically ElevenLabs' own recommended "alias" workaround and it works for the ~8 vowel-shift pairs I've mapped. Side benefit: since my cache key is a hash of (lang + text), two senses naturally get two cache entries.

It fails in three ways:

  1. Stress-shift pairs. REcord/reCORD, PREsent/preSENT, CONtent/conTENT. Respelling can't encode stress, and I haven't found a trick spelling that does.
  2. Monolingual. It's an English orthography hack. Nothing about it transfers to ES/PT/IT/FR, all of which have their own homographs.
  3. Manual. Hand-curated table. Doesn't scale to a few thousand words.

What I'm actually asking

  1. Is there a TTS API that accepts a sense/POS hint, or per-request phonemes, on a multilingual model? Or does everyone route heteronyms to a separate English-only model and eat the voice mismatch?
  2. If IPA is the only real answer — has anyone found v3-class IPA reliable enough in production? The docs quote 80–90% consistency, which for a teaching app means the wrong pronunciation ships to a learner one time in eight.
  3. For stress-shift specifically: any orthographic trick that works, or is phoneme-level control genuinely the only path?
  4. On the STT side — is there a mobile-viable way to get phonemes rather than words? I've looked at wav2vec2 phoneme-CTC or a forced aligner with GOP scoring via ONNX on-device, but I don't know if that's realistic on a mid-range phone or if I'm about to spend a month learning that it isn't. Whisper doesn't help; it also returns orthography.
  5. The unglamorous option: detect heteronyms and simply disable pronunciation scoring for them, with an honest note to the user. Is that what shipped apps actually do?

If you've built pronunciation feedback into anything real, I'd love to know where you drew the line between "graded properly" and "good enough." Happy to share code for any of the above.

reddit.com
u/Fair_Expression_3291 — 2 days ago

Built and shipped 4 apps for iOS and Android without ever owning a Mac

Wrapped up something this week I wasn't sure I'd pull off. Four apps, all built on a Windows machine, all now live on both the App Store and Google Play. Never owned or borrowed a Mac for any of it.

The Windows-to-iOS part is the thing people don't expect. You don't actually need a Mac to ship an iOS app anymore. I built everything in React Native with Expo, and the iOS binaries compile in the cloud through EAS. Expo runs the Mac build infrastructure on their end, hands you back a signed .ipa, and you submit it to the App Store with no physical Apple machine anywhere in the loop. The Android builds went through the same pipeline. After years of assuming "iOS means buy a Mac," that still feels a little unreal.

So that's four apps on both stores, all off one Windows laptop. Two word games and two for learning Python, if you're curious. Getting them through Google's closed testing gate and Apple's review was its own slog, but they're all public now.

Honest part: building them turned out to be the achievable bit. The wall I'm at now is getting anyone to actually find and use them. Downloads are basically zero, which I gather is the normal starting line, so I'm deep in the figuring-out-users phase and it's humbling.

If you're building on Windows and stuck on the "but I don't have a Mac" thing, happy to answer whatever about the EAS setup. It genuinely erased the biggest blocker I thought I had. And if you want to see what I actually shipped, I'll drop links in a comment.

reddit.com
u/Fair_Expression_3291 — 5 days ago

Got all four of my apps through closed testing and live on Play

Been lurking here through the whole closed testing thing, so figured I'd post now that it's finally done. All four of my apps cleared the testing requirement and they're live on Play as of today.

Building them was never the hard part. The wall was keeping enough testers opted in for fourteen straight days without the group quietly falling apart. Someone installs it, opens it once, forgets it exists, and you're sitting there hoping nobody uninstalls before the two weeks are up.

What got me through: family and friends to start, and when the group thinned out I paid a few testers to hold the line. Recruiting them wasn't really the problem. Keeping twelve people opening an app they've got zero reason to care about, every couple days for two weeks, is the part that actually wears you down. Nobody warns you about that one.

Couple things that bit me, in case it saves someone. Testers have to stay opted in the whole window, so if one opts out or wipes the app halfway through it can set you back. And a silent install doesn't do much on its own, you want them actually opening it so there's activity. I gave up trusting people to remember and just messaged them directly every few days.

Doing this four times back to back was its own special kind of tired.

Not pretending it's a finish line though. Clearing the gate just means Google lets you hit publish, it doesn't mean a single person finds the app. Downloads and retention are the next wall and I'm staring straight at it now. But the testing part does end, so if you're mid-fourteen-days as you read this, it's survivable.

How'd everyone else keep testers active through the window? Paid testers, tester-swap groups, just bugging friends?

reddit.com
u/Fair_Expression_3291 — 5 days ago

Got all four of my apps through closed testing and live on Play

Been lurking here through the whole closed testing thing, so figured I'd post now that it's finally done. All four of my apps cleared the testing requirement and they're live on Play as of today.

Building them was never the hard part. The wall was keeping enough testers opted in for fourteen straight days without the group quietly falling apart. Someone installs it, opens it once, forgets it exists, and you're sitting there hoping nobody uninstalls before the two weeks are up.

What got me through: family and friends to start, and when the group thinned out I paid a few testers to hold the line. Recruiting them wasn't really the problem. Keeping twelve people opening an app they've got zero reason to care about, every couple days for two weeks, is the part that actually wears you down. Nobody warns you about that one.

Couple things that bit me, in case it saves someone. Testers have to stay opted in the whole window, so if one opts out or wipes the app halfway through it can set you back. And a silent install doesn't do much on its own, you want them actually opening it so there's activity. I gave up trusting people to remember and just messaged them directly every few days.

Doing this four times back to back was its own special kind of tired.

Not pretending it's a finish line though. Clearing the gate just means Google lets you hit publish, it doesn't mean a single person finds the app. Downloads and retention are the next wall and I'm staring straight at it now. But the testing part does end, so if you're mid-fourteen-days as you read this, it's survivable.

How'd everyone else keep testers active through the window? Paid testers, tester-swap groups, just bugging friends?

reddit.com
u/Fair_Expression_3291 — 5 days ago

My WebView crash-recovery logic was the crash. Running real CPython in an Expo app

Context: kids' Python learning app, Expo / RN 0.81 / React 19. Kids type Python and it actually runs — Pyodide inside a hidden 0×0 react-native-webview mounted once at the app root.

WebView content processes die. On iOS it's usually a WKWebView OOM (onContentProcessDidTerminate); on Android it's onRenderProcessGone. So I wrote the obvious recovery: process dies → mark not-ready → reload the WebView → Pyodide re-boots → back in business. Worked great in testing.

Then I hit a device where Pyodide couldn't boot at all.

Reload → die → reload → die. The recovery path was the crash. Main thread saturated, Hermes OOM'd, hard crash at roughly 80 seconds every time a kid opened the Cadet path. My self-healing runtime was the thing killing the app.

Fix was bookkeeping, not cleverness:

js

// Keep only reloads from the last 12s
bridge.reloadTimes = bridge.reloadTimes.filter((t) => now - t < 12000);

// Debounce: terminate + renderProcessGone can both fire for ONE death
if (last && now - last < 1000) return;

// Cap: 3 reloads in the window means the boot itself is broken. Stop.
if (bridge.reloadTimes.length >= 3) { bridge.reloadsExhausted = true; /* ... */ }

Past the cap it stops reloading, resolves every pending run with "The code workshop couldn't start — please reopen the app," and leaves the app alive and navigable. A healthy boot clears the counter so a later unrelated crash still gets its full budget. Degraded and honest beats dead.

Unrelated bonus rake in the same feature: the code editor is also a WebView. A native multiline TextInput OOM-crashes the JS thread the moment Fabric commits it on this stack (RN 0.81 + React 19 + New Arch). A <textarea> never touches Fabric's text machinery, so it sidesteps the whole thing and I get a synced line-number gutter for free.

So: two WebViews, one visible and one hidden, in an app for six-year-olds. Not the architecture I set out to write.

Anyone else hit the Fabric multiline TextInput crash, or find a fix that isn't "use a WebView"?Context: kids' Python learning app, Expo / RN 0.81 / React 19. Kids type Python and it actually runs — Pyodide inside a hidden 0×0 react-native-webview mounted once at the app root.

WebView content processes die. On iOS it's usually a WKWebView OOM (onContentProcessDidTerminate); on Android it's onRenderProcessGone. So I wrote the obvious recovery: process dies → mark not-ready → reload the WebView → Pyodide re-boots → back in business. Worked great in testing.

Then I hit a device where Pyodide couldn't boot at all.

Reload → die → reload → die. The recovery path was the crash. Main thread saturated, Hermes OOM'd, hard crash at roughly 80 seconds every time a kid opened the Cadet path. My self-healing runtime was the thing killing the app.

Fix was bookkeeping, not cleverness:

js
// Keep only reloads from the last 12s
bridge.reloadTimes = bridge.reloadTimes.filter((t) => now - t < 12000);

// Debounce: terminate + renderProcessGone can both fire for ONE death
if (last && now - last < 1000) return;

// Cap: 3 reloads in the window means the boot itself is broken. Stop.
if (bridge.reloadTimes.length >= 3) { bridge.reloadsExhausted = true; /* ... */ }

Past the cap it stops reloading, resolves every pending run with "The code workshop couldn't start — please reopen the app," and leaves the app alive and navigable. A healthy boot clears the counter so a later unrelated crash still gets its full budget. Degraded and honest beats dead.

Unrelated bonus rake in the same feature: the code editor is also a WebView. A native multiline TextInput OOM-crashes the JS thread the moment Fabric commits it on this stack (RN 0.81 + React 19 + New Arch). A <textarea> never touches Fabric's text machinery, so it sidesteps the whole thing and I get a synced line-number gutter for free.

So: two WebViews, one visible and one hidden, in an app for six-year-olds. Not the architecture I set out to write.

Anyone else hit the Fabric multiline TextInput crash, or find a fix that isn't "use a WebView"?

u/Fair_Expression_3291 — 15 days ago
▲ 9 r/react+2 crossposts

A race between app launch and RevenueCat was silently downgrading my paying users to free

This one annoyed me for a while because it never happened on my phone, only on real users'.

Some people who'd paid for Pro would open LexiShuffle and get the free version. Not every time. Just sometimes, right when the app opened. Which of course meant I couldn't reproduce it, because on my device everything's warm and instant.

It's a race, and if you've done RevenueCat you can probably already guess. Checking whether someone has Pro is a network call. My game screen doesn't wait for it, it just mounts and starts a round. So if that check hasn't come back yet, the game looks at your status, sees nothing, goes "ok, free user" and gives you a free-tier round. Premium category, quietly downgraded to free, because the entitlement landed like half a second after the screen mounted.

The person who paid me got the free experience. Great.

Fix was basically don't start the round until you actually know. I've got a flag now:

resolved = !statusQuery.isLoading || Boolean(purchases.isPremium)

so it's true once the status check finishes (success or error, doesn't matter) or RevenueCat already says you're Pro. The game sits on its loading screen until that's true, and it's in the effect deps so the second it flips, the effect re-runs and sets the round up properly. The thing I was paranoid about was it getting stuck false and hanging people on a spinner forever, but the query always settles so it can't.

Then I remembered offline exists. No signal means the check literally can't finish, so an offline Pro user just never resolves and eats the same downgrade permanently. So there's a little per-account cache now that remembers "this account was Pro last time" and trusts that while you're offline.

Anyway. If you do IAP, it's the fun kind of bug that's invisible to you and only screws the people paying you. Anyone got a nicer way to handle the offline entitlement thing? Caching last-known-Pro feels a bit gross but I couldn't think of better.

reddit.com
u/Fair_Expression_3291 — 20 days ago

Spent a week on a Hermes OOM crash. It was a TextInput aliased back to itself in Metro

Stack: Expo SDK 54, RN 0.81, Hermes, New Architecture. Posting because this ate a week and the crash log was pointing right at the cause the whole time while I looked everywhere else.

The symptom: the app crashed in TestFlight, but only on two screens. Multiplayer and account. Everywhere else was fine. And it didn't crash on open. You'd land on one of those screens, use it for a bit, and about fifty seconds later the whole app would die. Same timing every time.

The crash was a Hermes GC out-of-memory, SIGABRT inside the garbage collector. The frames were all object spreads and computed property writes piling up inside promise microtasks (hermesBuiltinCopyDataProperties, putComputed_RJS, DictPropertyMap growing). So I read it the obvious way. Something is building a massive object and blowing the heap. Both screens hit the network, both have forms, so a runaway response or a giant state blob felt right.

I chased the big object. Capped API responses at 1MB. Set structuralSharing to false on the React Query client so it would stop cloning data on every update. Both screens had a Ken Burns background component, so I put a flag on it and shipped a build with the animations off. Nothing changed. Still fifty seconds, still dead.

And every attempt is a full EAS build. Fifteen or twenty minutes to compile in the cloud, wait on Apple, install, watch it crash at the same spot, repeat. I'm on Windows with no Mac, so there's no local iteration for this. Every guess costs half an hour minimum. It wears you down.

None of it was the cause. It was a Metro alias. The TextInput component was aliased to a module that re-exported TextInput, and that resolved back through the same alias. It pointed at itself. So the moment any screen with a text field mounted, the component started spreading its own props into a new copy of itself, over and over, each pass a little bigger, until memory ran out. That's why every frame was an object spread. That's why it was only the two screens with text inputs. The fifty seconds was just how long the loop took to eat the heap.

The trace was honest the whole time. Object copies with no end, memory climbing. I kept translating it as "find the huge object" instead of "something is copying itself forever," because a runaway response was the bug I already expected on those screens.

Fixed the alias, both screens went quiet, and I turned the animations back on that I'd killed for nothing.

If you ever get a Hermes OOM where the frames are wall-to-wall object spreads and only certain components trigger it, check your Metro aliases for a loop before you spend a week on your data layer. Anyone else run into a self-referential alias? Still not totally sure how mine ended up in the config.

EDIT / correction: someone asked how I fixed the alias, and going back through the git history to answer properly, I have to correct this. The self-referential alias was a red herring. It was gated to web only, so it never ran on native, which means removing it fixed nothing. The real cause was a memory-heavy word-bundle download and parse that re-ran on every network resume and grew the heap until Hermes OOMed. The full word set was already baked into the app, so that download was redundant. The fix was guarding that pipeline and using the baked-in set. That's also why it was only the two network-heavy screens and why it took about 50 seconds, it was tracking a resume, not a render. Leaving the original write-up up so the comments still make sense, but the root cause I gave is wrong. Credit to the commenter who said to look at the data layer, which is exactly where it was.

reddit.com
u/Fair_Expression_3291 — 22 days ago

How are solo devs actually getting 12 testers for 14 days? Genuine question.

Solo dev, four apps sitting in closed testing. The 12-testers-for-14-continuous-days requirement is turning out to be harder than building the apps was, and I want to hear how people are actually clearing it without gaming it.

The part that gets me isn't finding 12 installs. It's the "active for 14 straight days" bit. Friends and family will happily tap the opt-in link and install, then never open it again, and that doesn't count. So I'm not really recruiting testers, I'm asking people to build a two-week habit around an app they have no reason to care about yet.

What I've tried:

- Personal network. Gets me installs, not sustained use. Most go dormant by day three.

- A recruitment email with the opt-in links and clear steps. Better, but the drop-off is still steep once the novelty is gone.

What I'm trying to avoid is the tester-swap groups. Reciprocal installs from strangers who don't care feel like exactly the kind of thing Google eventually decides was inauthentic, and I'd rather not build my launch on that.

So, for people who've actually gotten a personal developer account to production:

- Where did your 12 real testers come from?

- How did you keep them opening the app for the full two weeks? Reminders, a group chat, something else?

- Does a slightly engaged tester who opens it twice count, or does Google want genuine daily-ish use?

- Anyone run all their apps through one shared tester pool, and did that cause problems?

Not looking for a swap. Looking for how you did it for real.

reddit.com
u/Fair_Expression_3291 — 24 days ago

Four apps on the App Store, never owned a Mac. What building iOS on Windows actually costs you.

Four of my apps are on the App Store and I've never owned a Mac. All of it built on Windows.

People assume the hard part is the build. It isn't, not anymore. EAS compiles on a Mac in the cloud so I never open Xcode. The cost is everywhere else.

Mostly it's the feedback loop. No simulator on Windows, no plugging in an iPhone to debug like I can on Android. So checking one change means pushing a build, waiting in the queue, waiting for Apple to process it, then opening it on my phone. Twenty minutes to see a thing. On Android it's instant with a cable. Same code, and the difference in how fast you can move is enormous.

The rest is store friction that has nothing to do with code. Apple approves the build, then rejects the submission days later over a screenshot size or a permission string. The worst one was an in-app purchase bug on Apple's end that broke a whole subscription group, and the fix was to throw it out and rebuild it from scratch. Lost days I couldn't have saved.

Still glad I do it this way. You don't need to buy a Mac to ship on iOS. Just know the code is the easy part, and budget your patience for everything around it instead.

reddit.com
u/Fair_Expression_3291 — 25 days ago

Filtering false synonyms out of a WordNet-derived dataset using definition embeddings — does this hold up?

Question about using definition embeddings as a synonym-quality filter.

Context: bilingual word game, pairs derived from Princeton WordNet plus the Spanish WordNet through Open Multilingual Wordnet. Roughly 12.7k English and 8.1k Spanish after cleanup.

Structural defects were easy (taxonomic Latin, cross-language contamination, prompt/answer stem overlap). The residue is semantic: pairs in the same synset that no ordinary speaker accepts as synonyms.

The idea I want sanity-checked: embed each word's dictionary definition, score pairs by cosine similarity between the two definition vectors, and use low scores to flag likely-bad pairs for review. Rationale being that true synonyms get defined with overlapping vocabulary and synset-adjacent pairs don't.

Where I expect it to break:

- Near-synonyms whose definitions are deliberately contrastive. Frugal vs stingy, or any pair a lexicographer bothered to distinguish. These are exactly the pairs I want to keep and I think this method would flag them.

- WordNet glosses are shared across a synset, so they carry no signal for this. Needs an independent source. Wiktionary presumably, but Spanish coverage worries me.

- Definitions are short. Not sure sentence embeddings behave well on 8-word strings with heavy function-word overlap.

Has anyone tried this, and did it survive contact with real data? I only need a ranking signal to prioritize manual review, not a classifier.

reddit.com
u/Fair_Expression_3291 — 27 days ago
▲ 3 r/LinguisticsDiscussion+1 crossposts

Cleaning 22k synonym pairs for a bilingual word game — how do you QA meaning at scale?

Spending today on Lexícon's word library — the bilingual synonym bank the whole game runs on.

The first version was derived: Princeton WordNet plus the Spanish WordNet via Open Multilingual Wordnet, difficulty tiers assigned by word frequency. That got me to roughly 22,000 pairs quickly, and taught me that "fast" and "shippable" are different things.

What the cleanup passes actually caught:

— Linnaean binomials. "Parus atricapillus" is in the data. It is not a word anyone is going to guess.

— Cross-language leaks — English lemmas sitting inside the Spanish bank.

— Structural giveaways: prompt and answer sharing a stem (northerly → northward), clean substrings (playact → act), morphological suffixes (bluish → blue). Free answers.

— Narrow-sense pairs — technically synonymous in one specific WordNet sense, wrong in ordinary use.

Four rounds on the English filter logic before I chose aggressive removal over more iterations. Losing a few borderline-good pairs is cheap when thousands remain; another review cycle isn't.

Where I've landed: authoring original entries in batches of 100 — my own definitions, my own pairs — instead of deriving more from corpora. Slower, but the quality is controlled and the content is unambiguously mine.

The part I haven't solved: semantic quality doesn't automate. No heuristic reliably separates "these are real synonyms" from "these share a synset" without throwing out good pairs alongside the bad. I shipped an in-app report button so players can flag broken pairs, but that's cleanup after the fact, not prevention.

So, for anyone who's shipped a content-heavy game or app:

  1. How do you QA semantic correctness at scale without hand-checking every row?

  2. Has native-speaker review worked for you in any form that isn't just paying for it?

  3. Do player-report loops actually produce signal, or mostly noise?

Genuinely curious what's worked for people.

reddit.com
u/Fair_Expression_3291 — 29 days ago