Mosaics: first-vowel anchoring and honest empties

Anchoring at the first vowel instead of the stressed rime lets
tonight rebuild as "a night"; single-letter left words return with a
rank penalty; indexes warm at boot; the no-results message stops
blaming syllable count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 04:21:45 -04:00
parent 20f722e5c9
commit e45fbf6eb3
3 changed files with 12 additions and 5 deletions
+9 -4
View File
@@ -32,6 +32,7 @@ async def lifespan(app: FastAPI):
except Exception:
pass
get_slant_index()
get_mosaic_indexes()
yield
@@ -1133,7 +1134,12 @@ def mosaics_for(w: str, limit: int) -> list[dict]:
phones = phones_for(w)
if not phones:
return []
rime = DIGITS.sub("", pronouncing.rhyming_part(phones)).split()
# anchor at the FIRST vowel: tonight (AH N AY T) splits into a + night
pl = DIGITS.sub("", phones).split()
vi = next((i for i, p in enumerate(pl) if p in ARPA_VOWELS), None)
if vi is None:
return []
rime = pl[vi:]
pidx, ridx = get_mosaic_indexes()
pairs: dict[str, float] = {}
for i in range(1, len(rime)):
@@ -1147,15 +1153,14 @@ def mosaics_for(w: str, limit: int) -> list[dict]:
# (placement -> place + meant, the EH collapsing to a schwa)
tails = {right: 2.0, _squeeze(right): 0.0}
for a in ridx.get(left, []):
if len(a) < 2:
continue
for tail_key, bonus in tails.items():
for b in pidx.get(tail_key, []):
if a == w or b == w:
continue
phrase = f"{a} {b}"
score = (zipf_frequency(a, "en")
+ zipf_frequency(b, "en") + bonus)
+ zipf_frequency(b, "en") + bonus
- (1.5 if len(a) < 2 else 0))
if score > pairs.get(phrase, 0):
pairs[phrase] = score
ranked = sorted(pairs.items(), key=lambda kv: (-kv[1], kv[0]))
+1 -1
View File
@@ -655,7 +655,7 @@ async function doLookup(){
const data = await r.json();
if(mode === 'mosaic'){
if(!data.known){
resultsBox.innerHTML = `<p class="muted">No two-word mosaics for “${esc(word)}” — try a word with two or more syllables.</p>`;
resultsBox.innerHTML = `<p class="muted">No clean two-word split for “${esc(word)}” — mosaics need a sound the dictionary can rebuild from two words (try placement, creation, tonight).</p>`;
return;
}
renderMosaics(word, data.words);
+2
View File
@@ -525,6 +525,8 @@ def test_mosaic_generator():
assert "place meant" in words
creation = {m["word"] for m in mosaics_for("creation", 20)}
assert "way shun" in creation
tonight = {m["word"] for m in mosaics_for("tonight", 20)}
assert "a night" in tonight
def test_word_info():