mirror of
https://github.com/kennethreitz/rhymepad.org.git
synced 2026-06-11 17:08:33 +00:00
Remove the Mosaics tool; def card stops rebuilding on submode clicks
The two-word generator goes (the /api/word phonetics card stays); the definition card now belongs to the word — switching Rhymes/ Synonyms only swaps the list, and a dismissed card stays dismissed until a new word is looked up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,6 @@ async def lifespan(app: FastAPI):
|
||||
except Exception:
|
||||
pass
|
||||
get_slant_index()
|
||||
get_mosaic_indexes()
|
||||
yield
|
||||
|
||||
|
||||
@@ -1094,79 +1093,6 @@ def _ranked(words, exclude: set[str], limit: int) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
_phones_index: dict[str, list[str]] | None = None
|
||||
_rime_index: dict[str, list[str]] | None = None
|
||||
|
||||
|
||||
SQUEEZABLE = {"AH", "IH", "EH", "UH", "ER"} # vowels that reduce unstressed
|
||||
|
||||
|
||||
def _squeeze(phones: str) -> str:
|
||||
return " ".join("x" if p in SQUEEZABLE else p for p in phones.split())
|
||||
|
||||
|
||||
def get_mosaic_indexes():
|
||||
"""full-phones -> words (exact and vowel-squeezed) and rime -> words,
|
||||
normalized, built once."""
|
||||
global _phones_index, _rime_index
|
||||
if _phones_index is None:
|
||||
pronouncing.init_cmu()
|
||||
pidx: dict[str, list[str]] = defaultdict(list)
|
||||
ridx: dict[str, list[str]] = defaultdict(list)
|
||||
for w, phones in pronouncing.pronunciations:
|
||||
if not w.isalpha() or zipf_frequency(w, "en") < 3.0:
|
||||
continue
|
||||
ph = _norm_r(phones)
|
||||
bare = DIGITS.sub("", ph)
|
||||
pidx[bare].append(w)
|
||||
squeezed = _squeeze(bare)
|
||||
if squeezed != bare:
|
||||
pidx[squeezed].append(w)
|
||||
ridx[DIGITS.sub("", pronouncing.rhyming_part(ph))].append(w)
|
||||
_phones_index, _rime_index = pidx, ridx
|
||||
return _phones_index, _rime_index
|
||||
|
||||
|
||||
def mosaics_for(w: str, limit: int) -> list[dict]:
|
||||
"""Two-word phrases that rhyme with w perfectly: split its rime at
|
||||
every consonant boundary and search both halves (placement ->
|
||||
race + meant)."""
|
||||
phones = phones_for(w)
|
||||
if not phones:
|
||||
return []
|
||||
# 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)):
|
||||
left, right = " ".join(rime[:i]), " ".join(rime[i:])
|
||||
if not any(p in ARPA_VOWELS for p in rime[:i]):
|
||||
continue
|
||||
if not any(p in ARPA_VOWELS for p in rime[i:]):
|
||||
continue
|
||||
# exact tail match, plus squeezed: where the target carries a
|
||||
# reducible vowel, the tail word's own vowel may squeeze to fit
|
||||
# (placement -> place + meant, the EH collapsing to a schwa)
|
||||
tails = {right: 2.0, _squeeze(right): 0.0}
|
||||
for a in ridx.get(left, []):
|
||||
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
|
||||
- (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]))
|
||||
return [{"word": p, "syl": None} for p, _ in ranked[:limit]]
|
||||
|
||||
|
||||
@app.get("/api/word")
|
||||
def word_info(word: str):
|
||||
"""Phonetic anatomy of a word: phones, syllables, stress, rime."""
|
||||
@@ -1192,9 +1118,6 @@ def lookup(word: str, mode: str = "rhyme", limit: int = 60):
|
||||
sections = synonyms_for(w, limit)
|
||||
return {"word": w, "mode": mode, "known": bool(sections),
|
||||
"sections": sections}
|
||||
if mode == "mosaic":
|
||||
words = mosaics_for(w, limit)
|
||||
return {"word": w, "mode": mode, "known": bool(words), "words": words}
|
||||
phones = phones_for(w)
|
||||
if not phones:
|
||||
return {"word": w, "mode": mode, "known": False, "words": []}
|
||||
|
||||
+10
-19
@@ -298,7 +298,6 @@ Double-click any word to look it up on the right."></textarea>
|
||||
<div class="seg" id="modeSeg">
|
||||
<button class="active" data-mode="rhyme">Rhymes</button>
|
||||
<button data-mode="syn">Synonyms</button>
|
||||
<button data-mode="mosaic" title="Two-word phrases that rhyme with it">Mosaics</button>
|
||||
</div>
|
||||
<div id="defBox"></div>
|
||||
<div id="lookupResults">
|
||||
@@ -648,18 +647,18 @@ async function doLookup(){
|
||||
const word = document.getElementById('lookupInput').value.trim();
|
||||
if(!word) return;
|
||||
document.getElementById('tab-lookup').scrollTop = 0;
|
||||
showDefinition(word); // the definition always pins on top
|
||||
// the card belongs to the word, not the submode: rebuild only when
|
||||
// the word changes, and respect a dismissal until then
|
||||
if(word.toLowerCase() !== defWord){
|
||||
defWord = word.toLowerCase();
|
||||
defDismissed = false;
|
||||
showDefinition(word);
|
||||
}
|
||||
resultsBox.innerHTML = '<p class="muted">Searching…</p>';
|
||||
try{
|
||||
const r = await fetch(`/api/lookup?word=${encodeURIComponent(word)}&mode=${mode}`);
|
||||
const data = await r.json();
|
||||
if(mode === 'mosaic'){
|
||||
if(!data.known){
|
||||
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);
|
||||
}else if(mode === 'syn'){
|
||||
if(mode === 'syn'){
|
||||
if(!data.known){
|
||||
resultsBox.innerHTML = `<p class="muted">No synonyms found for “${esc(word)}”.</p>`;
|
||||
return;
|
||||
@@ -694,6 +693,7 @@ function wireChips(){
|
||||
|
||||
/* ---------- definition card (free dictionary API) ---------- */
|
||||
const defBox = document.getElementById('defBox');
|
||||
let defWord = null, defDismissed = false;
|
||||
function defCard(word, inner){
|
||||
defBox.innerHTML = `
|
||||
<div class="defcard">
|
||||
@@ -705,7 +705,7 @@ function defCard(word, inner){
|
||||
</div>
|
||||
</div>`;
|
||||
defBox.querySelector('#defInsert').addEventListener('click', ()=> insertAtCursor(word));
|
||||
defBox.querySelector('.defx').addEventListener('click', ()=>{ defBox.innerHTML=''; });
|
||||
defBox.querySelector('.defx').addEventListener('click', ()=>{ defBox.innerHTML=''; defDismissed = true; });
|
||||
}
|
||||
function draftMates(word){
|
||||
if(!analysis) return [];
|
||||
@@ -759,15 +759,6 @@ function renderChips(label, words){
|
||||
resultsBox.innerHTML = `<div class="res-label">${label}</div>` + chipHtml(words.slice(0,50));
|
||||
wireChips();
|
||||
}
|
||||
function renderMosaics(word, items){
|
||||
let h = `<div class="res-label">two-word rhymes for “${esc(word)}” — click to insert</div>`;
|
||||
h += chipHtml(items.map(d=>d.word));
|
||||
resultsBox.innerHTML = h;
|
||||
resultsBox.querySelectorAll('.chip').forEach(c=>{
|
||||
c.addEventListener('click', ()=> insertAtCursor(c.dataset.w));
|
||||
});
|
||||
}
|
||||
|
||||
function renderSections(word, sections){
|
||||
if(!sections.length){ resultsBox.innerHTML = `<p class="muted">No synonyms for “${esc(word)}”.</p>`; return; }
|
||||
let h = '';
|
||||
|
||||
@@ -519,16 +519,6 @@ def test_secondary_pronunciation_stays_local():
|
||||
|
||||
# -------------------------------------------------------------- new tools
|
||||
|
||||
def test_mosaic_generator():
|
||||
from app import mosaics_for
|
||||
words = {m["word"] for m in mosaics_for("placement", 20)}
|
||||
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():
|
||||
from app import word_info
|
||||
info = word_info(word="tonight")
|
||||
|
||||
Reference in New Issue
Block a user