70.1.21. pepsi-stage-detect-language

detect the human language of a message body

Manual section:

1

70.1.21.1.1. Name

pepsi-stage-detect-language - the language-detection stage of the Pepsi pipeline.

70.1.21.1.2. Synopsis

pepsi-stage-detect-language [GLOBAL-OPTIONS] worker

70.1.21.1.3. Description

pepsi-stage-detect-language is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It loads that pepsi.ingress row (refusing to act unless its status is running), reads its [stage-<stage>] section, identifies the human language(s) used in the message body, and records them under state.language before advancing the message to NEXT_STAGE. This stage never drops, bounces or pauses a message, and it preserves the rest of state.

The stage extracts the message’s inline body text with a full MIME parser (shared logic in pepsi-common): text/plain parts are taken verbatim and text/html parts are reduced to plain text, each decoded from its content-transfer-encoding and its declared character set to UTF-8. Attachments (including text attachments) are ignored, as are encrypted or otherwise opaque bodies (for example multipart/encrypted or application/pkcs7-mime), which carry no readable text. For a multipart/alternative the plain alternative is preferred over the HTML one — except when that plain alternative turns out to hold no prose at all, the shape a bulk mailer produces when it puts two bare URLs where the message should be; the HTML sibling is then used instead.

70.1.21.1.3.1. Prose filtering

The extracted text is then reduced to the part of it that is prose, and only that is classified. A statistical detector models how humans write words, and an e-mail body is full of things that are not words: URLs, addresses, base64 keys, DNS records, hexadecimal identifiers. Those are not merely noise. Measured over real mail, a body that is mostly machine text is classified from the machine text: a DNS-record notification and a social-network follow notice both came out Yoruba, and a spam body of two bare URLs came out Tsonga, when every word a human had written in them was English.

Two filters run, in this order:

  • tokens that cannot be words are dropped — anything containing @, / or an internal full stop, anything beginning http or www., anything less than half letters, anything containing a digit, anything longer than 30 characters, and anything whose capitalisation changes inside the word (CamelCase, base64);

  • lines left with fewer than 15 letters, or without a letter majority, are dropped — what filter one leaves of a DNS dump or a table is a scatter of bare field names, which is not prose in any language.

The rules are aware of scripts that do not separate words with spaces (Han, Kana, Hangul, Thai, Khmer, Lao, Burmese, Tibetan): there a whole clause arrives as one token, so the length, digit, full-stop and capitalisation rules are not applied to it, and lines are judged by how many letters they hold rather than how many words. Without that, a Chinese or Thai message would be deleted in its entirety.

70.1.21.1.3.2. Classification

The surviving prose is classified with the lingua statistical detector, built from the candidate languages named by the LANGUAGES option. The detector yields a probability for each candidate language; the most likely at most three languages whose probability is at least 5 % are recorded.

If the message has no usable inline text, none of that text is prose, or there is too little prose (fewer than 20 characters) to classify reliably, the stage skips classification: the message advances unchanged, with no language key added. Recording nothing is deliberate — a body that is only URLs has no language, and guessing one from a URL slug is how a message ends up confidently tagged with a language nobody wrote it in.

70.1.21.1.4. Output format

state.language is a string in the syntax of an HTTP Accept-Language header: a comma-separated list of code;q=quality items, ordered from most to least likely. Each code is the language’s ISO 639-1 code and each quality is its detection probability in the range 01 (trailing zeros trimmed, so a certain single language reads q=1). For example, a message that is mostly English with some German might be recorded as:

en;q=1, de;q=0.45

The detector is expensive to construct, so each worker process builds it once (per distinct candidate-language set) and reuses it for every message. It is built at lingua’s full accuracy; the library’s low-accuracy mode is deliberately not used. That mode short-circuits — it returns a language at probability 1 as soon as exactly one candidate owns an n-gram occurring anywhere in the text, without examining the rest — which is how an address in a quoted attribution line turned three kilobytes of English into Estonian, and a base64 DKIM key turned a DNS-change notification into Yoruba. With prose filtering in place it also measured slower on long bodies (the shortcut search is paid for and then discarded) and larger in resident memory (it preloads extra n-gram count models), so it buys nothing.

70.1.21.1.5. Configuration

Options live in the stage’s own [stage-<name>] section (PROGRAM = pepsi-stage-detect-language): a NEXT_STAGE and the candidate LANGUAGES set. They are documented in pepsi.conf(5).

70.1.21.1.6. State

Inputs: none from state; the stage reads the stored message body.

Outputs: when at least one language is detected, the stage merges language (the Accept-Language-style string) into state and advances; otherwise state is left untouched. The state layout is described in pepsi.state(7).

Transitions: always advances to NEXT_STAGE, whether or not a language was detected. There is no branch; the stage never pauses, fails, reroutes or finishes.

70.1.21.1.7. Commands

worker

Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.

70.1.21.1.8. Global Options

-c FILE, –config FILE

Read the configuration from FILE instead of searching the default locations.

-L LOGLEVEL, –log LOGLEVEL

Set the logging verbosity (default info).

-v, –verbose

Show log messages from all sources.

-h, –help; -V, –version

Print a usage summary / the version and exit.

70.1.21.1.9. Exit Status

0

The message was processed (advanced, with or without a recorded language).

1

An error occurred (message not found or not running, misconfigured stage — e.g. an invalid LANGUAGES code — or a database error). The reason is written to the log.

70.1.21.1.10. Diagnosis and tuning

pepsi-detect-language(1) is the offline counterpart of this stage: the same executable under a second name, which classifies a message read from a file or standard input and prints only what this stage would have recorded, without going near the database. Use it to reproduce a misclassification from a saved message (--explain shows the extracted text, the prose left after filtering it, and every candidate’s confidence) and to try candidate sets before configuring one (--languages).

70.1.21.1.11. Examples

Classify one message by hand (the id must name a running row):

echo 42 | pepsi-stage-detect-language -c /etc/pepsi/pepsi.conf worker

A pipeline section that tags the body language before the whitelist check:

[stage-detect-language]
PROGRAM = pepsi-stage-detect-language
NEXT_STAGE = check-whitelist
LANGUAGES = en de fr es

70.1.21.1.12. See Also

pepsi-detect-language(1), pepsi-config(1), pepsi-stage-check-whitelist(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)

70.1.21.1.13. Bugs

Report bugs to the Pepsi issue tracker.