Ask whether text is plausibly Danish, instead of what language it is - #707
Merged
Conversation
The check has been asking a detector to name a winner and then arguing with the
answer. That framing is what all the machinery was for, and the framing was wrong.
Naming a winner cannot separate "wrong language" from "close neighbour": short
Danish often wins as Norwegian, so the module carried a hand-maintained list of
confusable families to rule that Norwegian was near enough to Danish to allow. The
list forgave real defects with the misreads — a Norwegian learner served a Danish
script passed it.
lingua scores a named language directly, so the question can be the one we
actually have. Correct Danish is 0.6-0.99 plausible Danish and the neighbours
never come up; English in a Danish lesson is 0.01. Given a paragraph the
neighbours separate too — Danish is 4e-09 plausible Norwegian — so the wrong
Scandinavian language is now caught rather than excused.
Measured over the 6532 stored lessons before committing to it: this flags 8 where
langdetect flagged 12, and the four it drops were sound Dutch and Danish that
langdetect read as Afrikaans. All three known bad dialogues stay caught. Cost is
+19MB resident and 0.2ms a line.
Two levels, named for where they apply. SENTENCE_LEVEL_FLOOR asks whether one
piece of text is plausible. LESSON_LEVEL_THRESHOLD asks how much of a lesson may
fail before the lesson does — because a four-word line is unreliable even here
("Er det i kantinen?" is 0.12 correct Danish), and aggregation is what makes an
unreliable signal usable. Each level sets its own minimum for what it will judge
at all: 3 words for a line inside a lesson, 60 characters for a field checked
alone, which has nothing to average against.
Gone with the old framing: _CONFUSABLE_FAMILIES, _accepted_codes, _chunk,
passage_mismatch, PASSAGE_CHUNK_CHARS, MAX_WRONG_SHARE, _dialogue_lines,
MIN_CHARS_FOR_DETECTION, LANGDETECT_SUPPORTED. Every one existed to make a
winner-take-all signal usable in aggregate.
Adaptive simplification now checks each part of a level separately. Glued, a
level's Danish summary masked its English body: 0.97 plausible as Danish for a
field whose content alone scored 0.00.
langdetect stays in the other eleven modules that use it. They ask an open
question — what language is this article? — where naming a winner is the job.
The two one-off tools that produced this evidence go too. The dump showed the
false positives were all drill fragments; the comparison showed lingua wins on the
whole corpus, and that a strict got != want per line was mine, not lingua's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… skip Two review findings, both introduced by the rewrite. The lesson-level threshold is a share, and nothing said how many lines it had to be a share of. Correct short Danish sits under the floor routinely — "Jeg bor i et hus." is 0.16, "Er det i kantinen?" is 0.12 — which is fine among twenty siblings and fatal among two: a lesson with a brief dialogue came back flagged "2 of 2 lines", both lines sound. The corpus that calibrated 0.45 had 10-30 lines per lesson. Below 200 characters of judged text there is no cushion, so the answer is "can't judge", logged. The share stays line-based rather than weighted by length, because that is what the 0.45 was measured against; changing the measure would invalidate the calibration. And the rewrite dropped the line that announces "no model to check this against", added this morning for the reason the module exists: a silent skip reads exactly like a successful check. Restored at the decision point rather than inside plausibility(), so a lesson logs once instead of once per line. Both are pinned by tests now. The log line had none, which is why it evaporated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Replaces langdetect with lingua in
language_check— and with it, the framing the module was built on.The wrong question
The check asked a detector to name a winner and then argued with the answer. That can't separate wrong language from close neighbour: short Danish often wins as Norwegian, so the module carried a hand-maintained list of confusable families ruling that Norwegian was near enough to Danish to allow. The list forgave real defects along with the misreads — a Norwegian learner served a Danish script passed it.
lingua scores a named language, so the question can be the one we actually have: is this plausibly Danish?
Measured on all 6532 stored lessons before committing to it
All three known-bad dialogues (235, 236, 238) stay caught. lingua's three remaining flags are one coherent class —
Sauron,week-end,Il rooftop— lessons whose target word is foreign, so the lines genuinely contain foreign text.Cost: +19 MB resident, 0.2 ms per line.
Two levels, named for where they apply
SENTENCE_LEVEL_FLOOR(0.2) — is this piece of text plausible?LESSON_LEVEL_THRESHOLD(0.45) — how much of a lesson may fail before the lesson does?The second exists because a four-word line is unreliable even here: correct Danish
Er det i kantinen?scores 0.12. Aggregation is what turns an unreliable per-line signal into a trustworthy verdict — so individual failures don't need to be right, only the share.Each level sets its own minimum for what it will judge at all: 3 words for a line inside a lesson, 60 characters for a field checked alone, which has nothing to average against.
What this deletes
_CONFUSABLE_FAMILIES,_accepted_codes,_chunk,passage_mismatch,PASSAGE_CHUNK_CHARS,MAX_WRONG_SHARE,_dialogue_lines,MIN_CHARS_FOR_DETECTION,LANGDETECT_SUPPORTED. Every one existed to make a winner-take-all signal usable in aggregate. Net −24 lines and two fewer tuned numbers, with better results.Adaptive simplification also now checks each part of a level separately: glued, a level's Danish summary masked its English body — 0.97 plausible as Danish for a field whose content alone scored 0.00.
Scope
langdetectstays in the other eleven modules that use it. They ask an open question — what language is this article? — where naming a winner is exactly the job. Onlylanguage_checkasks a closed one.Two integration details worth knowing: lingua splits Norwegian into Bokmål and Nynorsk, so Zeeguu's
nomaps toNB(without it every Norwegian lesson would be unjudgeable), and Kurdish has no model there either, so it stays "can't judge" exactly as before.Testing
zeeguu/core/test— 259 passed. The tests changed shape in one telling place:test_scandinavian_neighbours_are_not_flagged_against_each_otherasserted that a Danish script passes as a Norwegian lesson. That encoded a langdetect limitation as a requirement, and it's now inverted — a Danish script in a Norwegian lesson is a defect, and we can finally tell.🤖 Generated with Claude Code