Class JunkDetector
- All Implemented Interfaces:
TextQualityDetector
Scoring combines several features:
- Codepoint-bigram log-probability (z1) — every bigram is
bucketed to its script (
forEachScriptBigram(int[], JunkDetector.BigramSink)) and scored against that script's open-addressed bigram table; unseen pairs fall back to a unigram independence-assumption scoreα * (log P(cp_a) + log P(cp_b)). This is the only per-script feature. - Unicode block-transition log-probability (z2) — a single
global table over
Character.UnicodeBlockvalues. - Control-byte fraction (z3) — fraction of bytes in control ranges [0x01–0x08, 0x0B, 0x0C, 0x0E–0x1F, 0x7F].
- Script-transition log-probability (z4) — a single global
table over raw
Character.UnicodeScriptvalues. - Document-level quality features z5..z9 (letter-adjacent-to-mark, replacement ratio, script density, script coherence, script alternation).
Per-script z1 is calibrated (mu/sigma) on held-out dev text; z2..z9 are
global. The z-scores are combined by a single global linear combiner:
logit = w1*z1 + ... + w9*z9 + bias, fit on clean vs. corrupted
windows. Natural junk threshold is 0 (positive logit = clean); use
negative thresholds for conservative detection.
Model file format: a single binary spec (see load(InputStream)
javadoc). No backwards-compat fallback to older formats — the loader
rejects mismatched version bytes with a clear error. This is
intentional: keeping parallel scoring paths is a known source of silent
miscalibration bugs.
Instances are immutable and thread-safe after construction.
Typical usage:
JunkDetector detector = JunkDetector.loadFromClasspath();
TextQualityScore score = detector.score("some text");
if (score.getZScore() < 0) { ... flag as junk ... }
// Arbitrate between two charset decodings
TextQualityComparison result = detector.compare("cp1252", ascp1252, "cp1251", ascp1251);
String winner = result.winner(); // returns "cp1252" or "cp1251"
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceSink forforEachScriptBigram(int[], JunkDetector.BigramSink): (modelScript, cpA, cpB).static final classPer-feature z-score breakdown returned byscoreWithFeatureComponents(String). -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringModel script key for the pooled COMMON (digits/punctuation/symbols) table.static final StringClasspath resource path for the bundled production model.static final intstatic final intSentinel "codepoints" (above the Unicode maximum U+10FFFF, so they cannot collide with real text) that wrap each letter token: TOKEN_START (^) before the first letter of a run and TOKEN_END ($) after the last.static final intSole supported file-format version. -
Method Summary
Modifier and TypeMethodDescriptionbucketSumsAndCounts(int[] cps, Map<String, BigramTables> tablesByScript) Per-script{sumLogP, count}buckets for a codepoint sequence, scored with the given per-script tables.static intcodepointToIndex(BigramTables tables, int cp) Binary-search a codepoint in the script's index.Compares two candidate strings and returns which is higher-quality (cleaner text).static doublecomputeF1MeanLogP(int[] cps, BigramTables tables) Codepoint-array form ofcomputeF1MeanLogP(String, BigramTables).static doublecomputeF1MeanLogP(String text, BigramTables tables) Mean log-prob over the codepoint pairs intextusing the given script's bigram tables.static floatcomputeZ2BlockTransition(String text, float[] blockTable, float[] blockCal) Feature 2 — calibrated z-score for block-transition mean log-prob on one text window.static floatcomputeZ3ControlByte(byte[] utf8, float[] controlCal) Feature 3 — calibrated z-score for control-byte fraction on the UTF-8 byte sequence of one text window.static floatcomputeZ4ScriptTransition(String text, float[] scriptTransTable, float[] scriptTransCal, Map<String, Integer> scriptBucketIndex, int numScriptBuckets) Feature 4 — calibrated z-score for global script-transition mean log-prob on one text window.floatz5: calibrated letter-adjacent-to-mark ratio.floatz6: calibrated replacement-character ratio.floatz9: calibrated script-alternation ratio.static voidforEachScriptBigram(int[] cps, JunkDetector.BigramSink sink) intReturns the file-format version of the loaded model (alwaysVERSION; mismatches are rejected at load time).Returns the set of script names this model knows about.static JunkDetectorload(InputStream rawIs) Loads a model from anInputStream.static JunkDetectorLoads the bundled model from the classpath.static JunkDetectorloadFromPath(Path path) Loads a model from the given file path.static intpackBigramKey(int idxA, int idxB) Packed bigram key for indices(a, b)where each index fits inbits.invalid reference
JunkDetectorTrainingConfig#KEY_INDEX_BITSstatic JunkDetectorprovider()ServiceLoaderprovider hook (Java 9+).Scores the given string for text quality.Diagnostic — exposes the per-feature z-scores, the global combiner weights, and the dominant script.
-
Field Details
-
DEFAULT_MODEL_RESOURCE
Classpath resource path for the bundled production model.- See Also:
-
VERSION
public static final int VERSIONSole supported file-format version. Mismatch is a hard error — prior versions live in git history and are not loadable by this build. We deliberately don't keep dual-version paths so it's impossible to confuse model versions.- See Also:
-
COMMON_SCRIPT
Model script key for the pooled COMMON (digits/punctuation/symbols) table.- See Also:
-
TOKEN_START
public static final int TOKEN_STARTSentinel "codepoints" (above the Unicode maximum U+10FFFF, so they cannot collide with real text) that wrap each letter token: TOKEN_START (^) before the first letter of a run and TOKEN_END ($) after the last. Emitted byforEachScriptBigram(int[], JunkDetector.BigramSink)so the bigram LM learns word-initial / word- final letter typicality, and so z1 never empties for text containing even a single letter.- See Also:
-
TOKEN_END
public static final int TOKEN_END- See Also:
-
-
Method Details
-
loadFromClasspath
Loads the bundled model from the classpath.- Throws:
IOException- if the model resource is missing or malformed
-
provider
ServiceLoaderprovider hook (Java 9+). AllowsJunkDetectorto be registered as aTextQualityDetectorSPI implementation even though its construction goes throughloadFromClasspath()rather than a public no-arg constructor. -
loadFromPath
Loads a model from the given file path. The file may be gzipped or raw.- Throws:
IOException
-
load
Loads a model from anInputStream. Gzip-detection is automatic. Strictly requires the current file-format version (15) — older formats are rejected with a clear error rather than supported via a fallback path.File-format layout (gzipped):
[8 bytes] magic "JUNKDET1" (ASCII) [1 byte] version (= 15) [4 bytes] num_scripts (int BE) [1 byte] block_scheme_version (must equal
UnicodeBlockRanges.SCHEME_VERSION) // z4 — global script-transition section [1 byte] num_script_buckets for each bucket: [2 bytes] name length (ushort BE) [name bytes] bucket name (UTF-8) [4 bytes] scriptTrans_quant_min (float32 BE) [4 bytes] scriptTrans_quant_max (float32 BE) [num_script_buckets² × 2 bytes] script-transition table (z4, int16-quantized) [4 bytes] mu4 (z4 calibration, float32 BE) [4 bytes] sigma4 // z2 — global block-transition section [4 bytes] block_quant_min (float32 BE) [4 bytes] block_quant_max (float32 BE) [block_N² × 2 bytes] block-transition table (z2, int16-quantized) [4 bytes] mu2 (z2 calibration) [4 bytes] sigma2 // global per-feature calibrations, {mu, sigma} float32 pairs [8 bytes] z3 calibration (control-byte ratio) [8 bytes] z5 calibration (letter-adjacent-to-mark) [8 bytes] z6 calibration (replacement-char ratio) [8 bytes] z9 calibration (script-alternation) // global combiner [1 byte] num_features [(num_features+1) × 4 bytes] combiner weights w1..wN and bias // per-script section for each script (sorted by name): [2 bytes] name length [name bytes] script name (UTF-8) [4 bytes] mu1 (z1 calibration, codepoint-bigram mean log-prob) [4 bytes] sigma1 [variable] bigram + unigram tables — exact layout inBigramTables.writeTo(DataOutputStream): codepoint index, then the sorted-occupied bigram keys (key[0] as int32 BE followed by LEB128 varint deltas) and 8-bit quantized bigram and unigram log-prob values- Throws:
IOException
-
score
Scores the given string for text quality.The text is split into contiguous runs of the same Unicode script. Each run is scored against its own script model. Logits are combined as a byte-count-weighted average, so mixed-script text (e.g. half LATIN, half HAN) is scored fairly without arbitrarily picking one script. COMMON, INHERITED, and UNKNOWN codepoints (spaces, punctuation, digits) are attached to the preceding script run.
- Specified by:
scorein interfaceTextQualityDetector- Parameters:
text- the string to score; must not be null- Returns:
- a
TextQualityScore; checkTextQualityScore.isUnknown()if the input is empty or the script is not covered by the model
-
compare
public TextQualityComparison compare(String labelA, String candidateA, String labelB, String candidateB) Compares two candidate strings and returns which is higher-quality (cleaner text).A common use case is charset-decoding arbitration: given raw bytes decoded via two different charsets, pass each decoded string here with a human-readable label (e.g. the charset name) and the detector will pick the one that looks more like natural language.
Each candidate is scored independently via
score(String). The candidate with the higher score wins.An UNKNOWN score (script not in model) is treated as neutral (0) rather than
-∞. This prevents a garbled-but-recognisable decoding from beating a correct decoding whose script happens to be unknown to the model — for example, a pure-katakana zip entry name decoded as Shift-JIS (UNKNOWN) vs. the same bytes decoded as UTF-8 (garbled LATIN, negative z-score).- Specified by:
comparein interfaceTextQualityDetector- Parameters:
labelA- human-readable label for candidate A (e.g."cp1252")candidateA- first candidate stringlabelB- human-readable label for candidate B (e.g."cp1251")candidateB- second candidate string- Returns:
- a
TextQualityComparisonwith the winning label and confidence delta
-
knownScripts
-
getModelVersion
public int getModelVersion()Returns the file-format version of the loaded model (alwaysVERSION; mismatches are rejected at load time). -
scoreWithFeatureComponents
Diagnostic — exposes the per-feature z-scores, the global combiner weights, and the dominant script. Same math asscore(String). -
computeZ5LetterAdjacentToMarkRatio
z5: calibrated letter-adjacent-to-mark ratio. Delegates raw computation toTextQualityFeatures.letterAdjacentToMarkRatio(String)and applies the document-level (mu, sigma) calibration loaded from the model file. Returns 0 (neutral) when the model has no z5 calibration or when the raw value is NaN.Positive z5 = correct decoding of a precomposed-or-decomposed script (Vietnamese, Indic, Thai, Arabic). Negative z5 = mojibake of such content as Latin-1.
-
computeZ6ReplacementRatio
z6: calibrated replacement-character ratio. Direct decode-failure signal — fraction of codepoints that are U+FFFD. Higher raw value = more decode failure = junkier; but the calibration centers on the training distribution, so negative z6 = junkier than typical.Returns 0 (neutral) when no calibration available.
-
computeZ9AlternationRatio
z9: calibrated script-alternation ratio. Catches the mojibake-of- Latin-as-CJK pattern where every accent becomes a singleton Han char scattered through Latin text (high alternation = max value). Length- and proportion-invariant by construction. Sign flipped so clean (low alternation) → positive z9 and mojibake (high alternation) → negative. -
computeZ2BlockTransition
Feature 2 — calibrated z-score for block-transition mean log-prob on one text window. Returns 0 if the window has fewer than two codepoints or ifblockTable/blockCalare null.Block bucketing is via the JVM-independent
UnicodeBlockRanges. Public so the trainer's classifier feature extractor calls into the exact same math used at inference time — single source of truth, no train/infer drift.- Parameters:
blockTable-(blockN)² × floatlog-prob table whereblockN = UnicodeBlockRanges.bucketCount()
-
computeZ3ControlByte
public static float computeZ3ControlByte(byte[] utf8, float[] controlCal) Feature 3 — calibrated z-score for control-byte fraction on the UTF-8 byte sequence of one text window. Stored score is-fractionso higher = cleaner (matching the direction convention of the other z-features).Public for train/infer math-sharing.
-
computeZ4ScriptTransition
public static float computeZ4ScriptTransition(String text, float[] scriptTransTable, float[] scriptTransCal, Map<String, Integer> scriptBucketIndex, int numScriptBuckets) Feature 4 — calibrated z-score for global script-transition mean log-prob on one text window. Uses rawCharacter.UnicodeScriptvalues (no model fallback) so HIRAGANA / KATAKANA / HAN remain distinct. Returns 0 if the window has fewer than two non-neutral codepoints or if the script-transition data isn't supplied.Public for train/infer math-sharing. Note: inference computes z4 once per document via
computeScriptTransitionZ(String)(which uses the instance's loaded tables); this helper takes them as arguments so training can compute z4 before the model is finalised. -
computeF1MeanLogP
Mean log-prob over the codepoint pairs intextusing the given script's bigram tables.For each adjacent codepoint pair
(a, b):- Binary-search both codepoints in the script's codepoint index.
If either is absent, the pair was never seen in training; emit
α * (logP(a) + logP(b))using each codepoint's unigram value (orBigramTables.unigramFallbackLogProbif the codepoint isn't even in the unigram index). - Otherwise, look up the packed
(idxA<<16)|idxBkey in the open-addressing bigram table. Empty slot → unseen pair → unigram backoff (same formula). Match → dequantize the stored value.
This is the single authoritative implementation of the bigram scoring math, shared by inference and training. Keeping one implementation eliminates the risk of train/infer drift in the F1 feature.
- Returns:
- mean log-prob, or
Double.NaNiftexthas fewer than two codepoints ortablesis null
- Binary-search both codepoints in the script's codepoint index.
If either is absent, the pair was never seen in training; emit
-
computeF1MeanLogP
Codepoint-array form ofcomputeF1MeanLogP(String, BigramTables).Operates on a pre-decoded codepoint sequence rather than a
Stringso callers (e.g.forEachScriptBigram(int[], JunkDetector.BigramSink)bucketing) can score arbitrary codepoint pairs without re-encoding. Same math — this is the single authoritative implementation; theStringoverload just decodes and delegates. -
codepointToIndex
Binary-search a codepoint in the script's index.- Returns:
- the dense index (≥ 0) if found, or -1 if the codepoint doesn't appear in any kept bigram for this script
-
packBigramKey
public static int packBigramKey(int idxA, int idxB) Packed bigram key for indices(a, b)where each index fits inbits. Asserts that indices are non-negative; that's the caller's contract.invalid reference
JunkDetectorTrainingConfig#KEY_INDEX_BITS -
bucketSumsAndCounts
public static Map<String,double[]> bucketSumsAndCounts(int[] cps, Map<String, BigramTables> tablesByScript) Per-script{sumLogP, count}buckets for a codepoint sequence, scored with the given per-script tables. This is exactly the z1 inputaggregate(String)computes, exposed so the trainer's calibration and the inference scorer cannot drift. -
forEachScriptBigram
-