One weak phoneme, three words, and a model that cannot invent them
After a practice session on Vocalace I have phoneme scores for a handful of words, and I want three words that drill the weakest sound. Ask a model for those words and it will name one that is not in the dictionary, repe
After a practice session on Vocalace I have phoneme scores for a handful of words, and I want three words that drill the weakest sound. Ask a model for those words and it will name one that is not in the dictionary, repeat one, or add a sentence of advice. None of that is a list I can store. The model does not search the dictionary. Code chooses the sound, a query builds a short list, and the model may only fill the slots that are still empty. A parser then keeps a reply only when every token is in that list, once, at the exact length.
Three words, one sound
This is the fixture the ranker test locks in.
| word | focus | score |
|---|---|---|
| sorry | s | 50 |
| think | th | 61 |
| thing | th | 62 |
s is the worse score. It still loses. Votes are one per word, and th has two. A second take on sorry does not add a vote: the same word keeps a single focus, the worse of the two. A recording under 40 never reaches this table. If every remaining focus is green, there is no sound to drill, and the model is not called.
return b.count - a.count
|| b.redCount - a.redCount
|| a.minScore - b.minScore
|| left[0].localeCompare(right[0])
Count comes first. Red versus yellow is only the tie break, then the lower minimum score, then the phoneme id.
think and thing already exposed th, so they occupy two of the three slots, lower score first: think, then thing. One slot is left. That is the only call.
What the model is allowed to see
The open slot comes from a query. The SAPI token has to match as a whole token, (^| )th( |$). The word needs a common part of speech. Words already seen today are removed. The query stops at 20 rows. That array is the entire set of legal answers.
The prompt receives the phoneme, how many words are still missing, and those rows. It asks for words where the sound is easy to hear on its own, and for no explanation. The parser does not score that preference. A clumsy word that is actually in the list will be stored. Membership, uniqueness, and length are the whole check:
export function parseWordsFromAskPick({ raw, allowed, need }: {
raw: string
allowed: string[]
need: number
}): string[] | null {
const byWord = new Map(allowed.map(word => [word.toLowerCase(), word]))
const picked: string[] = []
for (const item of raw.trim().split(/[|๏ฝ]/)) {
const word = byWord.get(item.trim().toLowerCase())
if (!word || picked.includes(word)) {
return null
}
picked.push(word)
}
return picked.length === need ? picked : null
}
The parser tests use a three-word list so the failures are obvious. allowed is cat, hat, map.
| raw | need | result |
|---|---|---|
cat |
1 | ['cat'] |
hat๏ฝmap |
2 | ['hat', 'map'] |
dog |
1 | null |
cat |
2 | null |
cat๏ฝcat |
2 | null |
dog was produced. It is not in allowed, so the reply is discarded. cat when two words were required is discarded for length. cat๏ฝcat is discarded because the second token repeats the first. Any bad piece rejects the whole reply. A trailing bar does the same: the empty piece is not in the map.
On the real slot, need is 1 and allowed is those 20 rows. Month matches month only if month is one of them. thick matches only if thick is one of them. month because the th is clear is a single token after the split on |, the lookup misses, and the reply is null.
A null result is tried again, up to four times. After that the job fails. The bad text is not trimmed down to the pieces that happened to match.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.