@@ -290,13 +290,32 @@ private Charset arbitrate(TikaInputStream tis,
290290 }
291291
292292 /**
293- * Generative-model tiebreaker: for each candidate charset's decoded text,
294- * detect the most likely language then compute its z-score. The charset
295- * producing the highest z-score (closest to "real language") wins, provided
296- * it exceeds {@link #MIN_GENERATIVE_ZSCORE}.
293+ * Generative-model tiebreaker: for each candidate charset's decoded
294+ * text, let the discriminative language classifier pick the most
295+ * likely language, then ask the generative model how natural the
296+ * decoded text is UNDER THAT LANGUAGE. The charset producing the
297+ * highest length-adjusted z-score wins, provided it exceeds
298+ * {@link #MIN_GENERATIVE_ZSCORE}.
297299 *
298- * @return the winning charset, or {@code null} if no candidate passes the
299- * threshold or all candidates decode to identical text
300+ * <p>The chaining matters. An earlier revision used
301+ * {@code GLM.bestMatch(text)} to pick the language, which compares
302+ * raw scores across all ~200 languages. That comparison is
303+ * unreliable because out-of-class inputs can produce hash-collision
304+ * scores that exceed in-class scores for another language — the
305+ * classic pathology where real Chinese prose ranks at position 20
306+ * under zho while Sakizaya, Amis, Min-Dong-romanization, and other
307+ * unrelated languages rank ahead of it. The GLM's raw scores were
308+ * never meant for across-language comparison. The discriminative
309+ * classifier was trained explicitly for that job and is far more
310+ * reliable at picking the language. Once a language is picked, the
311+ * GLM's per-language calibrated z-score answers the question it
312+ * actually was designed for: "is this decoded text natural text in
313+ * language X?"
314+ *
315+ * @return the winning charset, or {@code null} if no candidate
316+ * passes the z-score threshold, no candidate yields a
317+ * discriminative language prediction, or all candidates
318+ * decode to identical text
300319 */
301320 private static <K > K generativeTiebreak (Map <K , String > candidates ) {
302321 if (candidates .isEmpty ()) {
@@ -323,13 +342,20 @@ private static <K> K generativeTiebreak(Map<K, String> candidates) {
323342 if (CharSoupLanguageDetector .junkRatio (text ) > 0.10f ) {
324343 continue ;
325344 }
326- Map .Entry <String , Float > match = GLM .bestMatch (text );
327- if (match == null ) {
345+ // Chain: discriminative classifier picks the language,
346+ // GLM scores under that language (not bestMatch across all langs).
347+ List <String > topLangs =
348+ CharSoupLanguageDetector .topShortTextLanguages (text , 1 );
349+ if (topLangs .isEmpty ()) {
350+ continue ;
351+ }
352+ String discLang = topLangs .get (0 );
353+ if (discLang == null || discLang .isEmpty ()) {
328354 continue ;
329355 }
330- float z = GLM .zScoreLengthAdjusted (text , match . getKey () );
331- LOG .debug ("generativeTiebreak: {} -> lang ={} z={}" ,
332- entry .getKey (), match . getKey () , z );
356+ float z = GLM .zScoreLengthAdjusted (text , discLang );
357+ LOG .debug ("generativeTiebreak: {} -> discLang ={} z={}" ,
358+ entry .getKey (), discLang , z );
333359 if (!Float .isNaN (z ) && z > bestZ ) {
334360 bestZ = z ;
335361 bestKey = entry .getKey ();
0 commit comments