24c3533e1844ddb66ed456d455a336d18f2c7078
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
24c3533e18 |
Give read-aloud a Portuguese voice, and a slower one
Phase 21's infra half. Two things the pt-PT pair needs from TTS, and one thing every learner has wanted since Phase 11. **A language is no longer a code change.** The handler knew exactly two languages, named in the Config struct: English on TTS_ENDPOINT and Chinese on TTS_ENDPOINT_ZH. Petal now discovers its Piper instances from the environment — English keeps the unsuffixed pair it has always had, and every other language is a TTS_ENDPOINT_<LANG>/TTS_VOICE_<LANG> pair — so fr and es cost a compose service and two lines of .env. <LANG> is the base tag, because an environment variable name cannot hold pt-PT's hyphen and only one Portuguese model is loaded either way. A language configured by halves is dropped rather than routed: half a configuration should reach the client as "no voice here, use Web Speech", not as an instance that errors on every tap. The startup line now names the voices it actually resolved rather than the English endpoint it was handed — the same lesson the dictionary line learned last week. **pt_PT-tugão-medium is the only European voice Piper ships.** The other five pt models in the catalogue are Brazilian, so the default anyone reaches for is the wrong country — the same trap as `dictionary-pt` packaging VERO, arriving through the catalogue rather than through the model. Named explicitly in compose, with the query that checks it in the deploy README. **The slow replay** (SUGGESTIONS §5e) is `slow: true` on /api/tts, raising Piper's length_scale to ~4/3. Piper stretches durations rather than resampling, so it stays a voice instead of a groan. The pace is part of the cache key — without it the slow replay of a word already heard at normal speed would be served back at normal speed, which is the one request where the difference is the whole point. 🐢 sits beside 🔊 on the word card, the selection bubble and the garden flashcard; the Web Speech fallback slows too, so the button means the same thing when Piper is down. **And the other reading gets her own voice.** The `alsoIn` block — the Portuguese sense of a word that is also English — now speaks in the pair's locale, which the pack names (`locale`) rather than anything inferring it from the letters. "comum" is spelled identically in both halves; a detector would have to guess, and this is the same reason the gloss shows both directions instead of picking one. Tests: config discovery (both existing deployment shapes, half-configured languages dropped, the pre-map voice defaults preserved), the slow scale and its separate cache entry, pt routing on the base tag with pt-BR landing on the European instance, and speech.ts's request body. The i18n shape suite now asserts every pack names a speakable locale in its own language — and that pt-PT's is not pt-BR. Verified: go build/vet/test, tsc, vitest 125/125, vite build. Live smoke against two fake Piper servers: en/pt × normal/slow all reached the right instance at the right length_scale with four distinct cache entries, and an unconfigured language still 404s. |
||
|
|
ccb43e5a4d |
Phase 21: Petal learns to be an English+Portuguese pair
The plan said "Hunspell pt-PT vendored like en-US". Measuring that first is what saved it: nspell expands affixes eagerly on construction, and European Portuguese's 1,340 rules over 44,257 stems want over a gigabyte of browser heap — ~340 MB for the first 12,000 entries, and no return at all after three minutes on the whole file. So the expansion runs once at build time instead: 1,039,058 forms, 2.66 MB gzipped, read by the same nspell in 842 ms. The obvious npm package would also have shipped the wrong language. Both dictionary-pt and dictionary-pt-br carry VERO, the Brazilian word list, so vendoring by name puts pt-BR spellings behind a pt-PT label — the drift SUGGESTIONS §3 warns about, arriving through the packaging where no reviewer can see it. The source is Projecto Natura's, and the build script now asserts the fault lines (receção in, recepção out) before writing anything. Spellcheck consults both dictionaries and flags only what both reject, which is the no-detector answer to a pair with no script boundary. The word card does the same in the other direction: "data" is a word in both languages, so Petal shows both readings rather than guessing which she meant. Writing the tests caught the one real bug — extendedAlphabet was a snapshot while correct/suggest read live, and her dictionary arrives after English, so every lookup would have resolved "cora" while the underlines were already right. Not done, and not claimed: the pack has not been read by a pt-PT speaker, and the Piper voice is deferred with the deploy. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua |
||
|
|
97e9c269ec |
Phase 20: the dictionary stops being English and Chinese only
Word lookups now come from DreamDict's dict.db for every pair but Chinese — opened read-only beside petal.db, no service, nothing over the VPN, because a hover gloss has to answer in milliseconds. `Provider` is the two questions the popover and the tooltip already asked, so the embedded *Lexicon satisfies it with no changes at all; Set.For(lang) is the single place the choice between them is made. The prerequisite in the dreamdict repo turned out to be two things, not one: the module path was unfetchable *and* the query layer sat in internal/, which no other module may import whatever the module is called. Both fixed upstream. The plan's central assumption did not survive the data. It mapped Gloss ← Translate(word, "en", L1) one-to-one; against the real 452 MB database that table answers for 17% of the 2,000 commonest English words into pt-PT. Wiktionary's translation sections are thin in that direction — "ephemeral", "think" and "quickly" have no en→pt-PT row at all. Shared WordNet synsets answer for 61%, so DreamDict gained Equivalents() and Petal glosses through it. Ordering those was wrong in an instructive way too: sorting by frequency glosses "think" as lembrar, "remember", because lembrar is the commoner Portuguese word even though pensar shares six of think's synsets to lembrar's one. Counting sense agreement first asks the right question. The same measurement is why zh stays on ECDICT: DreamDict reaches a Chinese gloss for 53% of those words, ECDICT for nearly all of them. The plan said converge only if quality holds. It didn't, so nothing converged. Two decisions about failure worth keeping. A missing dict.db is not an error — a laptop checkout has never had one — but a present-and-never-imported one is, because that is a half-finished deploy. And a pt-PT writer with no dictionary falls back to the embedded datasets with the gloss suppressed, keeping definitions, synonyms and phonetics rather than blanking the popover: an empty field reads as "not found", the wrong language reads as broken. The new fields surface as an etymology line and a three-band chip. Three, not five: the difficulty score separates "everyday" from "you'll have to explain this" but cannot rank obfuscate against serendipity, and a finer scale would be a confident-looking lie. An unscored word gets no chip. Writing the tests found two bugs first — trimEtymology sliced by byte, which would have emitted invalid UTF-8 for exactly the Greek and Latin etymologies the feature exists for, and its ellipsis path overran its own cap. go build/vet/test, tsc, vite, vitest 96/96 clean; live smoke against the real dict.db with one instance flipped from zh to pt-PT mid-run. Not deployed: go.mod still replaces github.com/prosolis/dreamdict with ../dreamdict, so the Docker build needs the two upstream commits pushed and the replace dropped. The deployed dict.db also predates DreamDict's Spanish data. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua |
||
|
|
336cae93e0 |
Phase 19: the copy stops being hardcoded Mandarin
Every `中文 · English` string moves out of ~29 components into web/src/i18n: one Pack type, a verbatim zh pack, and two ways to read it — usePack() for components, pack() for the modules that build a line when something happens rather than when something renders. Anything with a value in it is a function on the pack rather than a template at the call site, English pluralisation included: word order isn't universal, and a pack author has to be able to move the number. The roster constants (tones, rewrite styles, export formats, companions) keep only value + emoji, so a label can't drift from its key. On the server, internal/llm/lang.go replaces "Simplified Chinese" in the three prompts that actually name her language. pt-PT is spelled "European Portuguese (pt-PT, never Brazilian Portuguese)" in the prompt itself, and each Lang carries her word for "why" so the tutor prompt still recognises the question when she asks it her way. pair_lang reaches the model through the row-scoped query each handler already ran — the one that proves she owns the document — rather than a second lookup that could disagree with it. Also records Phase 18's deploy: migration 0011 rehearsed against a copy of the live VPS database, then applied for real. |