Introduction
Glossia
A lossless linguistic codec for machine-to-human communication. Glossia generates natural-looking sentences using a context-free grammar (CFG), while embedding BIP39 mnemonic words in-order within the generated text.
Overview
This tool uses a small, controlled grammar to generate sentences that embed BIP39 words in their natural positions based on part-of-speech (POS) tags. The cover lexicon (non-payload words) is carefully constructed to avoid any BIP39 words, making decoding trivial: simply filter out all words that are not in the BIP39 word list.
A readable encoding, not steganography
Glossia is not a steganographic tool. Its goal is not to hide that data is present, but to make machine data human-friendly — something a person can read, speak, transcribe, and verify. This distinction drives the design:
- Steganography pays for concealment by spending nearly all of its capacity on cover text, typically netting well under 1 bit per symbol.
- Glossia hides nothing, so every payload word carries its full information content — ~11 bits with the 2048-word BIP39 list, up to ~15 bits with larger wordlists.
The payload is meant to be seen and decoded. The mission is to make ciphertext and other machine data — keys, signatures, hashes, mnemonics — human-friendly in a way existing formats (hex, Base64, fingerprints, ASCII armor) do not, without sacrificing too much security. When iterating on Glossia, optimize for legibility and density, not concealment.
Features
- CFG-based sentence generation: Uses a small context-free grammar to generate grammatically correct sentences
- POS-tagged payload embedding: Embeds BIP39 words based on their part-of-speech tags
- SFW cover lexicon: Uses a safe-for-work lexicon that excludes all BIP39 words
- Adaptive sentence length: Adjusts sentence complexity based on remaining payload tokens
- Word frequency analysis: Tool to generate word lists from frequency data with POS tags
Installation
Install from Source
# Clone the repository
git clone https://github.com/asherp/glossia.git
cd glossia
# Install the binary (the CLI lives in the glossia-cli crate)
cargo install --path glossia-cli
This will install the glossia binary to ~/.cargo/bin/glossia (or $CARGO_HOME/bin/glossia if set). Make sure this directory is in your PATH.
Note: The English language files (wordlists and grammars) are embedded in the binary, so no additional files need to be copied. For other languages (French, German), you'll need to provide the language files separately.
Install from Git
cargo install --git https://github.com/asherp/glossia.git glossia-cli
Verify Installation
glossia --help
Usage
Defaults and Use Cases
Glossia defaults to body grammar with natural length-mode, which is optimized for email body text and prose. The defaults are designed for different use cases:
-
Body grammar + natural mode (default): Best for email body text and prose. Generates longer, more natural sentences by sampling from the grammar's length distribution. This produces readable text that flows naturally.
-
Subject grammar + compact mode: Best for email subject lines where brevity is critical. Generates short phrases and may include prefixes (Re:, Fwd:, etc.). Uses compact mode to find the shortest possible sentence that fits the payload.
You can override these defaults using --dialect and --length-mode flags as needed.
Main Program
Basic Usage
Default: body grammar + natural length-mode
$ glossia --random 12 --seed 0
Snake set robot to the mixed ship. Each program is night to each ten shield.
Bamboo own way to the yellow.
The embedded words are: snake, robot, mixed, ship, program, night, ten, shield, bamboo, own, way, yellow. (Use --highlight bars to show delimiters like |snake|.)
Subject grammar (shorter sentences with prefixes)
$ glossia --random 12 --dialect subject --seed 0
A snake is out a robot is mixed the ship is pop the program is night the ten is
hot a shield is cut a bamboo is own a way is yellow
Compact body grammar
$ glossia --random 12 --dialect body --length-mode compact --seed 0
Snake see robot. Ban is mixed. Ship program tap. Night is ten.
Shield get bamboo. Pie own way. Yellow is set.
Provide words directly instead of random selection
$ glossia abandon ability able about above absent
The abandon may see the ability to each able ears about above. Guy may absent.
Encode an API key or secret
$ glossia --from-ascii "sk_live_51H3ll0W0r1d" --seed 0
Ban inflict foot to swallow for spray. Green may question. State get cinnamon.
Cricket see the gloom to army. The rid purpose already board the rid mosquito.
The API key is encoded into natural-looking prose. To decode, extract all BIP39 words from the output and convert them back to ASCII using the wordlist.
Encode an IP address
$ glossia --from-ascii "127.0.0.1" --dialect subject --length-mode compact --seed 0
A couple is out a museum is pop each slide is set a gate is ago a toast is bad
the blade is hot the series is big
IP addresses and other network identifiers can be encoded into prose. Using compact mode with subject grammar produces shorter, more concise output suitable for subject lines or when brevity is important.
Generate multiple variations and select the most compact
$ glossia --random 12 --variations 3 --seed 0
Snake set robot to the mixed ship. Each program is night to each ten shield.
Bamboo own way to the yellow.
Snake get robot to a mixed ship to program per the night ten.
Some shield see bamboo. Die may own way. The yellow might get pay.
Snake may see the robot. Each mixed ship program night.
Ten shield the bad bamboo. Each own way why yellow each cut.
=== Statistics ===
Input:
Total words: 12
POS breakdown:
Nouns: 10
Verbs: 6
Adjectives: 5
Adverbs: 1
Output:
Total words: 32
Payload words: 12 (37.5%)
Cover words: 20 (62.5%)
Sentences: 1
Avg words per sentence: 32.0
Compactness:
Score: 0.580 (58 BIP39 chars / 100 total chars)
Efficiency: 58.0% BIP39 characters
Variations:
Tested: 3
Min compactness: 0.523
Max compactness: 0.580
Avg compactness: 0.548
Improvement: 5.8% better than average
Additional Options
# Use a specific language (default: english)
glossia --random 12 --language english --seed 0
# Use a seed for reproducible output
glossia --random 12 --seed 0
# Show grammar rules
glossia --show-grammar --dialect body
# Verbose output for debugging
glossia --random 12 --verbose --seed 0
Command-Line Options
--random <N>: Generate sentences from N random BIP39 words--dialect <dialect>: Dialect to use:body(default) orsubject. Can include language:latin-body,english-subject,cs-nip04.body: Longer sentences, no prefixes. Best for email body text and prose.subject: Short sentences, may include prefixes (Re:, Fwd:, etc.). Best for email subject lines where brevity is critical.
--highlight <mode>: Highlight words:none(default),bars(| |), or color name--seed <N>: Seed for deterministic random generation--variations <N>: Generate N variations and select the most compact (default: 1)--language, -l <lang>: Language for wordlist:english(default, BIP39),french(BIP39),german--k-min <N>: Minimum sentence length in POS slots including Dot (default: 3)--k-max <N>: Maximum sentence length in POS slots including Dot (default: 20)--length-mode <mode>: Sentence length selection:compactornatural- Default:
body→natural,subject→compact compact: Try k from k_min to k_max, shortest first. Produces the shortest possible sentences.natural: Sample k from grammar's length distribution. Produces more natural, varied sentence lengths.
- Default:
--show-grammar: Display the grammar rules (then continue execution)--verbose, -v: Show detailed debugging information--help, -h: Show help message
How It Works
- Payload tokens: BIP39 words are tagged with allowed POS categories (Noun, Verb, Adjective, etc.)
- Grammar expansion: The CFG generates a stream of POS slots
- Slot filling: Payload tokens are embedded when they fit a slot's POS, otherwise cover words are used
- Decoding: Extract BIP39 words by filtering the output against the BIP39 word list
Important: Wordlists are append-only. You can add new words to wordlists, but you cannot replace or reorder existing words. This preserves backward compatibility with previously encoded messages, since Glossia encodes data by mapping words to their position in the wordlist. See the How It Works documentation for details.
Compact vs Natural (sentence length strategy)
There are (at least) two reasonable ways to drive sentence generation, depending on the UX you want:
-
Compact mode (minimize length):
- Generate the shortest possible sentence that can fit the payload.
- Practical approach: try (k = k_{\min}, k_{\min}+1, \dots) and stop at the first feasible (k).
- Within that fixed (k), pick the highest-probability sequence (tie-break by probability for readability).
-
Natural mode (maximize grammatical “naturalness”):
- Prefer sentence lengths and POS sequences that are more probable under the grammar.
- Practical approach: sample (or choose) (k) from the grammar’s length distribution, then sample a POS sequence proportionally to its probability.
If you want a single continuous “compactness ↔ naturalness” knob, a simple scoring function works well:
[ \text{score} = \log P(\text{sequence}) - \lambda \cdot \text{length} ]
Higher (\lambda) yields shorter, more compact text; lower (\lambda) yields more natural (but sometimes longer) text.
CLI default behavior: if you don't pass --length-mode, the program defaults to:
--dialect subject→compact--dialect body→natural
Grammar Files
Grammars are defined in CFG format files located in languages/{language}/ directories:
subject.cfg: Grammar for email subject lines (shorter, may include prefixes)body.cfg: Grammar for email body text (longer, more natural sentences)
The grammar parser uses the Pest parser generator to parse these CFG files. Grammar rules support weighted productions for probabilistic selection.
Grammar note: unbounded PP chaining via VP
The English grammar (languages/english/body.cfg) is set up so that VP can optionally end with one-or-more PPs (e.g., “… in the house on the hill …"). This removes the previous hard maximum sentence length (formerly capped at 13 POS terminals) while keeping PP itself as a simple unit (Prep NP).
Project Structure
This repository is a Cargo workspace:
glossia(repo root): the library crate (src/lib.rs, grammar, generator, codec, merkle, …), built ascdylib+rlibglossia-cli/: theglossiacommand-line binary (glossia-cli/src/main.rs)glossia-tools/: developer tooling binaries (cleanup_weights,tag_words,compare_pos_weights,get_top_words,validate_pos_weights)glossia-tools/py/: standalone Python helper scripts (Latin wordlist generation, payload dedup, PDF/word extraction). Run from the repo root, e.g.python glossia-tools/py/generate_latin_wordlist.py
Key files:
glossia-cli/src/main.rs: CLI entry point with arg parsing and generation/decoding flowsrc/lib.rs: Library root; providesGrammarCheckerfor nlprule integration and re-exports the public APIsrc/grammar.rs: Grammar parser and CFG implementation using pestsrc/grammar_parser.pest: Pest grammar definition for parsing CFG filesglossia-tools/src/bin/get_top_words.rs: Word frequency analysis tool for generating word listsglossia-tools/src/bin/tag_words.rs: POS tagging tool using nlprulelanguages/english/subject.cfg: CFG grammar definition for subject lineslanguages/english/body.cfg: CFG grammar definition for body textlanguages/english/cover_POS.txt: Cover lexicon with POS tags (format:word|POS1,POS2)languages/english/english_bip39_POS.txt: BIP39 word list with POS tagsCargo.toml: Rust project configuration with dependencies
Dependencies
rand = "0.8": For random selection of cover words and grammar productionsnlprule = "0.6": For natural language processing and POS tagginganyhow = "1.0": For error handlingpest = "2.7": For parsing CFG grammar filespest_derive = "2.7": Derive macro for pest parserserde = "1.0": Serialization frameworkserde_json = "1.0": JSON support for serdereqwest = "0.11": For downloading word frequency data (get_top_words)clap = "4.4": For command-line argument parsing (get_top_words)regex = "1.10": For POS tag parsing (get_top_words)flate2 = "1.0": For reading gzipped Ngram files (get_top_words)csv = "1.3": For parsing CSV frequency files (get_top_words)
Data Sources
For detailed information about the utility tools (word frequency analysis, POS tagging, POS weight generation), see the Tools documentation.
Language Support
The program supports multiple languages via the --language flag. Each language requires:
- A grammar file:
languages/{language}/{subject,body}.cfg - A POS-tagged BIP39 wordlist:
languages/{language}/{language}_bip39_POS.txt - A cover lexicon:
languages/{language}/cover_POS.txt
Currently supported languages:
english(default)french(requires POS-tagged wordlist)german(requires POS-tagged wordlist)
Notes
- The current lexicon is minimal and should be expanded for production use
- The grammar is intentionally simple and can be extended for more variety
- Payload words that don't fit available slots will trigger a warning
- Word frequency data is cached locally to avoid repeated downloads
- Wordlists are append-only: You can add words but cannot replace or reorder existing ones (see How It Works for why)
- See the Tools documentation for information about utility tools
- Grammar files use Pest parser syntax - see
src/grammar_parser.pestfor the grammar definition
Installation
From Source
# Clone the repository
git clone https://github.com/asherp/glossia.git
cd glossia
# Install the binary (the CLI lives in the glossia-cli crate)
cargo install --path glossia-cli
This installs the glossia binary to ~/.cargo/bin/glossia (or $CARGO_HOME/bin/glossia if set). Make sure this directory is in your PATH.
Note: The repository is a Cargo workspace. The
glossiacrate at the root is the library; theglossiacommand-line binary is provided by theglossia-clicrate, and the developer tooling (POS tagging, weight validation, etc.) lives inglossia-tools.
From Git
cargo install --git https://github.com/asherp/glossia.git glossia-cli
Verify Installation
glossia --help
What Gets Installed
The English language files (wordlists, grammars, and type definitions) are embedded in the binary at compile time, so no additional files need to be copied. Other languages with YAML files in the languages/ directory are also embedded in release builds.
In debug builds, only English is embedded for faster compile times.
WebAssembly (Browser)
Glossia can be compiled to WebAssembly and run entirely in the browser.
Prerequisites
Build
./build_web.sh
This runs wasm-pack build --target web --no-default-features --features wasm, copies the resulting glossia.js and glossia_bg.wasm into the web/ directory, and optionally runs wasm-opt -Os if available.
Serve Locally
python3 -m http.server -d web 8080
Then open http://localhost:8080/index.html.
Manual Build
If you prefer to run the steps yourself:
wasm-pack build --target web --no-default-features --features wasm
mkdir -p web
cp pkg/glossia_bg.wasm web/
cp pkg/glossia.js web/
Requirements
- Rust toolchain (1.70+)
- For POS tagging tools: nlprule binary data files (
en_tokenizer.bin,en_rules.bin) - For WASM builds: wasm-pack
Quick Start
Embed BIP39 Words in Prose
Provide words directly as arguments:
$ glossia abandon ability able about above absent
The abandon may see the ability to each able ears about above. Guy may absent.
The BIP39 words are embedded in-order within grammatically correct sentences. To decode, simply filter the output against the BIP39 wordlist.
Generate Random Words
$ glossia --random 12 --seed 0
Snake set robot to the mixed ship. Each program is night to each ten shield.
Bamboo own way to the yellow.
Encode ASCII Text
Encode arbitrary text (API keys, secrets, IP addresses) into natural language:
$ glossia --from-ascii "sk_live_51H3ll0W0r1d" --seed 0
Ban inflict foot to swallow for spray. Green may question. State get cinnamon.
Cricket see the gloom to army. The rid purpose already board the rid mosquito.
Decode Back to Text
$ glossia --decode ban inflict foot swallow spray green question state cinnamon cricket gloom army rid purpose already board rid mosquito
Highlight Payload Words
Use --highlight-payload to see which words carry the payload:
$ glossia --random 6 --seed 0 --highlight-payload bars
This wraps payload words in |delimiters| so you can visually distinguish them from cover words.
Next Steps
- Usage for a full CLI reference
- How It Works for the encoding/decoding theory
- Grammar for the grammar system
- Examples for more use cases
Usage
Input Modes
Glossia accepts payload words in three ways:
| Mode | Flag | Description |
|---|---|---|
| Direct words | glossia word1 word2 ... | Provide BIP39 words as positional arguments |
| Random | --random <N> | Generate N random BIP39 words |
| ASCII encoding | --from-ascii <text> | Encode arbitrary text to wordlist words |
For --from-ascii, use - to read from stdin:
echo "my secret" | glossia --from-ascii -
Decoding
Use --decode to convert payload words back to bytes (inverse of --from-ascii):
glossia --decode word1 word2 word3
Words can be provided as positional arguments or piped via stdin.
Dialect Modes
The --dialect flag selects the sentence generation style. It can include the language: --dialect latin-body, --dialect cs-nip04.
| Dialect | Default Length Mode | Description |
|---|---|---|
body (default) | natural | Longer sentences, natural prose. Best for email body text. |
subject | compact | Short sentences, may include prefixes (Re:, Fwd:). Best for subject lines. |
payload_only | N/A | Output only payload words, no cover words. |
Length Modes
The --length-mode flag controls how sentence length is chosen:
compact: Try lengths fromk_mintok_max, shortest first. Produces the most concise output.natural: Sample length from the grammar's probability distribution. Produces more varied, natural-sounding text.
If not specified, the default depends on the dialect: body uses natural, subject uses compact.
Highlighting
Payload Highlighting
--highlight-payload <mode> marks payload words in the output:
| Mode | Effect |
|---|---|
none (default) | No highlighting |
bars | Wraps payload words: |word| |
| Color name | ANSI color: red, green, blue, yellow, magenta, cyan, white, black |
| ANSI code | Numeric code (30-37) |
Merkle Highlighting
--highlight-merkle <mode> highlights Merkle proof words separately (only with --merkle). Same options as --highlight-payload.
Mad-lib Mode
--madlib replaces payload words with [POS] placeholders:
$ glossia --random 6 --seed 0 --madlib
This shows the grammatical structure without revealing the payload.
Merkle Trees
--merkle wraps N payload words in a Merkle proof structure, expanding them to 2N-1 words:
$ glossia --random 6 --merkle --seed 0
--demerkle extracts the original payload from a merkleized sequence:
$ glossia --demerkle word1 word2 word3 ...
Variations
--variations <N> generates N different embeddings and selects the most compact one. Prints statistics showing compactness scores:
$ glossia --random 12 --variations 3 --seed 0
Language and Wordlist
--language, -l <lang>: Language for grammar and wordlists. Default:english.--wordlist <name>: Wordlist to use. Options:bip39(default),ngram,hp.
Tuning Parameters
| Flag | Default | Description |
|---|---|---|
--k-min <N> | 3 | Minimum sentence length in POS slots (including Dot) |
--k-max <N> | 20 | Maximum sentence length in POS slots (including Dot) |
--width <N> | 80 | Line wrapping width |
--delimiter <str> | " " | Delimiter between words in payload_only mode |
--seed <N> | random | Seed for deterministic generation |
Other Flags
| Flag | Description |
|---|---|
--show-grammar | Display grammar rules, then continue execution |
--verbose, -v | Show detailed debugging information |
--help | Show help message |
Complete Command Reference
glossia [OPTIONS] [<word1> <word2> ... <wordN>]
Options:
--random <N> Generate N random payload words
--from-ascii <text> Encode ASCII text (use '-' for stdin)
--decode Decode words back to bytes
--dialect <dialect> body (default), subject, or payload_only
Can include language: latin-body, english-subject, cs-nip04
--length-mode <mode> compact or natural
--highlight-payload <mode> none, bars, or color name/code
--highlight-merkle <mode> Merkle word highlighting
--merkle Wrap payload in Merkle proof
--demerkle Extract payload from merkleized sequence
--madlib Replace payload with [POS] placeholders
--seed <N> Deterministic seed
--variations <N> Generate N variations, pick most compact
--language, -l <lang> Language (default: english)
--wordlist <name> Wordlist: bip39, ngram, hp
--k-min <N> Min sentence length (default: 3)
--k-max <N> Max sentence length (default: 20)
--width <N> Line wrap width (default: 80)
--delimiter <str> Delimiter for payload_only mode
--show-grammar Display grammar rules
--verbose, -v Debug output
--help Show help
Examples
Encoding a BIP39 Seed Phrase
Generate a 12-word seed phrase as natural prose:
$ glossia --random 12 --seed 0
Snake set robot to the mixed ship. Each program is night to each ten shield.
Bamboo own way to the yellow.
The embedded words are: snake, robot, mixed, ship, program, night, ten, shield, bamboo, own, way, yellow.
Compact vs Natural Mode
Natural mode (default for body grammar) produces longer, more varied sentences:
$ glossia --random 12 --seed 0
Compact mode produces the shortest possible output:
$ glossia --random 12 --dialect body --length-mode compact --seed 0
Snake see robot. Ban is mixed. Ship program tap. Night is ten.
Shield get bamboo. Pie own way. Yellow is set.
Subject Lines
Short phrases suitable for email subject lines:
$ glossia --random 12 --dialect subject --seed 0
A snake is out a robot is mixed the ship is pop the program is night the ten is
hot a shield is cut a bamboo is own a way is yellow
Encoding an API Key
$ glossia --from-ascii "sk_live_51H3ll0W0r1d" --seed 0
Ban inflict foot to swallow for spray. Green may question. State get cinnamon.
Cricket see the gloom to army. The rid purpose already board the rid mosquito.
Encoding an IP Address
$ glossia --from-ascii "127.0.0.1" --dialect subject --length-mode compact --seed 0
A couple is out a museum is pop each slide is set a gate is ago a toast is bad
the blade is hot the series is big
Highlighting Payload Words
See which words carry the payload with bars:
$ glossia --random 6 --seed 0 --highlight-payload bars
Or with color (terminal only):
$ glossia --random 6 --seed 0 --highlight-payload green
Mad-lib Mode
See the grammatical skeleton without payload words:
$ glossia --random 6 --seed 0 --madlib
Payload words are replaced with [POS] placeholders like [N], [V], [Adj].
Merkle Proofs
Wrap payload in a Merkle proof structure:
$ glossia --random 6 --merkle --seed 0
Extract the original payload from a merkleized sequence:
$ glossia --demerkle <merkleized words...>
Highlight both payload and Merkle words with different colors:
$ glossia --random 6 --merkle --highlight-payload green --highlight-merkle blue --seed 0
Multiple Variations
Generate 3 variations and select the most compact:
$ glossia --random 12 --variations 3 --seed 0
This prints all variations plus statistics:
=== Statistics ===
Input:
Total words: 12
POS breakdown:
Nouns: 10
Verbs: 6
Adjectives: 5
Adverbs: 1
Output:
Total words: 32
Payload words: 12 (37.5%)
Cover words: 20 (62.5%)
Compactness:
Score: 0.580 (58 BIP39 chars / 100 total chars)
Variations:
Tested: 3
Min compactness: 0.523
Max compactness: 0.580
Improvement: 5.8% better than average
Payload-Only Mode
Output just the payload words without grammar:
$ glossia --random 12 --dialect payload_only --seed 0
With a custom delimiter:
$ glossia --random 12 --dialect payload_only --delimiter ", " --seed 0
Reading from Stdin
Encode text piped from another command:
echo "my-api-key-12345" | glossia --from-ascii -
How It Works
Glossia encodes data by embedding words from a payload wordlist into natural-looking sentences generated by a context-free grammar. The encoding process maps data bits to word indices in the wordlist, which is why wordlists must be append-only.
Encoding Process
-
Payload tokens: BIP39 words (or words from your custom payload wordlist) are tagged with allowed POS categories (Noun, Verb, Adjective, etc.)
-
Grammar expansion: The CFG generates a stream of POS slots (e.g.,
Det N V NP Dot) -
Slot filling: Payload tokens are embedded when they fit a slot's POS, otherwise cover words are used
-
Decoding: Extract payload words by filtering the output against the payload wordlist
Wordlist Encoding
When encoding binary data (like ASCII text or API keys), Glossia uses bit-packing:
- Each word in the wordlist represents a unique index (0, 1, 2, ...)
- The number of bits per word is determined by the wordlist size:
bits_per_word = log2(word_count) - For example, a 2048-word list uses 11 bits per word
- Data bytes are packed into these bits, with multiple bytes potentially spanning across words
Frequency ordering for payload wordlists: When creating custom payload wordlists (not BIP39, which is frozen in alphabetical order), ordering words by frequency (most frequent first) provides encoding efficiency benefits. Common words appear at lower indices and are more likely to be selected during encoding, making the generated text more natural while maintaining the same information density. This is the same principle used for cover wordlists in Merkle mode.
Example
If your wordlist has 2048 words:
- Each word represents 11 bits of data
- The word at index 0 represents bits
00000000000 - The word at index 2047 represents bits
11111111111 - When encoding ASCII text, bytes are packed into these 11-bit chunks
BIP39 seed phrase example:
A 12-word BIP39 seed phrase contains 11 × 12 = 132 bits total:
- 128 bits of entropy + 4 bits of checksum = 132 bits
- Each of the 12 words encodes 11 bits of information
- This provides 128 bits of cryptographic entropy (sufficient for 128-bit security)
Density vs. steganography
This information density is what distinguishes Glossia from steganography, and the distinction is deliberate. A steganographic scheme hides the existence of data, and pays for that concealment by spending almost all of its capacity on cover — typically well under 1 bit per symbol. Glossia hides nothing: the payload is meant to be decoded, so every payload word carries its full log2(word_count) bits — 11 for BIP39, up to ~15 for larger lists. The goal is a readable encoding, not a hidden one — making machine data human-friendly without sacrificing too much security.
Encoding and Decoding via Wordlist
When encoding or decoding a payload, Glossia determines the effective wordlist size based on the actual words used in that payload.
Effective Wordlist Size
For a given series of payload words, the effective size of the wordlist is:
effective_size = max_index + 1
Where max_index is the highest index of any word actually used in the payload.
Why This Matters
This approach provides two key benefits:
-
Unambiguous decoding for existing payloads: When decoding a message, you only need to consider the words that were actually used. The effective wordlist size is determined by the maximum index present, ensuring that decoding is unambiguous even if the wordlist has grown since the message was encoded.
-
Future payloads access more entropy: As the wordlist grows (via appending new words), future payloads can use a larger vocabulary. This means they can encode more information per word:
- A 1024-word list (10 bits per word) can encode 10 bits per word
- A 2048-word list (11 bits per word) can encode 11 bits per word
- A 4096-word list (12 bits per word) can encode 12 bits per word
Entropy Growth Example
Example: Expanding the English BIP39 wordlist
The English BIP39 wordlist has 2048 words, which provides 11 bits of information per word (since 2^11 = 2048).
If developers decide to increase the wordlist by appending 2048 new English lemmas:
-
Initial wordlist: 2048 words (11 bits per word)
- Each word encodes 11 bits of information
- Maximum entropy: 11 bits per word
- A 12-word seed phrase contains 11 × 12 = 132 bits
-
After expansion: 4096 words (12 bits per word)
- Each word now encodes 12 bits of information
- Maximum entropy: 12 bits per word
- Gain: +1 bit per word for all payloads written after the expansion
- The same 132 bits can now be represented with 11 words instead of 12 (since 132 ÷ 12 = 11)
This means that as you expand your wordlist by appending new words, you're not just adding vocabulary—you're also increasing the information density of future encodings. Payloads encoded before the expansion continue to decode correctly using the smaller effective size (11 bits), while new payloads can take advantage of the increased entropy (12 bits) and be more compact.
Example: Glossia's Latin vocabulary
Glossia's Latin vocabulary has 2^16 = 65,536 words, providing 16 bits of information per word. This means:
- A 12-word BIP39 seed phrase contains 132 bits (11 × 12)
- The same 132 bits can be represented with just 9 Latin Glossia words (since 132 ÷ 16 = 8.25, requiring 9 words to encode 132 bits)
- This demonstrates how larger wordlists enable more compact encodings of the same information
Practical Example
Consider a wordlist that starts with 1000 words and grows to 2000 words:
-
Payload A (encoded when wordlist had 1000 words):
- Uses words with indices 0-999
- Effective size: 1000 words (10 bits per word)
- Decodes correctly even after wordlist grows to 2000 words
-
Payload B (encoded after wordlist grew to 2000 words):
- Can use words with indices 0-1999
- Effective size: 2000 words (11 bits per word)
- Encodes 1 more bit per word than Payload A
This demonstrates how the append-only constraint enables both backward compatibility and forward compatibility with increased entropy.
Glossia Translation
This is where Glossia's flexibility really shines: any Glossia-expressed bit stream can be converted to any other Glossia language in two simple steps:
- Decode from original language to binary: Extract payload words from the source text and convert them to their binary representation using the source language's wordlist
- Encode binary to target language: Convert the binary data to payload words using the target language's wordlist, then embed them in natural-looking sentences using the target language's grammar
How It Works
Since Glossia encodes data by mapping words to their indices in the wordlist, and those indices represent binary data, the translation process is straightforward:
Source Language Text
↓ (decode)
Binary Data (bits)
↓ (encode)
Target Language Text
The binary representation is language-agnostic—it's just bits. As long as both languages have wordlists that can represent the required bit range, translation is lossless and unambiguous.
Bijectivity
This translation results in no information loss, meaning that for a given bitstream all Glossia languages are bijective.
In mathematical terms, this means there exists a one-to-one correspondence (bijection) between any two Glossia languages for the same bitstream:
- Each unique bitstream maps to exactly one word sequence in each language
- All statements made in a language with the same order-preserving payload words will map back to exactly one bitstream
- Translation between languages preserves this one-to-one mapping
Furthermore, deterministic grammars ensure that every bitstream will map to one canonical representation for a given language.
This bijective property ensures that:
- No information is lost during translation
- No ambiguity exists—each bitstream has a unique word sequence in each language
- Round-trip translation (Language A → Language B → Language A) recovers the original exactly
- Commutative translation (Language A → Language B → Language C) yields the same result as (Language A → Language C)
Benefits
- Language independence: Encode once, translate to any supported language
- Lossless conversion: The binary representation preserves all information
- Flexible deployment: Choose the best language for your use case (compactness, readability, etc.)
- Future-proof: As new languages are added, existing encodings can be translated to them
Example Use Cases
- Compact encoding: Translate from English BIP39 (11 bits/word) to Latin Glossia (16 bits/word) for more compact representation
- Readability: Translate to a language with better grammar or more natural sentence structures
- Localization: Convert messages to languages appropriate for different regions or contexts
- Migration: Move existing encodings to newer, larger wordlists as they become available
The verb is authoritative: encode vs. transcode
The verb you use in a meta instruction decides how the input is interpreted, so intent is declared, not guessed:
-
encode into <language>— the input is raw data to encode. Glossia never inspects it for "is this secretly a seed or existing prose?" — that guessing is exactly what could silently corrupt ordinary text whose words happen to be payload words. So a bare list of payload words (even a full BIP39 mnemonic) given toencode into …is encoded as text and round-trips verbatim. -
transcode into <language>(ortranslate) — the input is an existing Glossia encoding. Glossia detects its source dialect and re-encodes. This is what compacts a seed into a denser language (a 12-word seed → ~9 Latin words). If no source encoding is detected, it errors and asks you to name the source withfrom <source>rather than guessing.
echo "<12-word seed>" | glossia --meta "encode into english" # → encoded as text, round-trips
echo "<12-word seed>" | glossia --meta "transcode into latin" # → detected as a seed, compacted to Latin
glossia --meta "transcode from latin into english" # explicit source, always works
The same input behaves differently under encode vs. transcode, but each is
an explicit instruction — there is no content-based heuristic that can misfire.
Two things bypass the verb, by design:
- Naming the source explicitly (
transcode from <source> into <target>) always wins — useful when auto-detection would be ambiguous. - Structured crypto formats (a
bc1…bech32 address, annpub…, etc.) are always recognized so they encode via their native alphabet. That is unambiguous format recognition, not seed/prose guessing.
When the target is a Format (the decode direction, e.g. decode into hex),
Glossia always detects which language the input prose is in — there, identifying
the source is the whole job.
Glossia Transmission
While direct-to-direct transmission is lossless, noise may be introduced while in-transit. For this reason, Glossia-encoded messages should always utilize checksums, error correction, and any additional information necessary for delivery.
Error Handling and Payload Integrity
However, note that because the decoding process is lossless, all corrections to the bitstream are included in the payload, not outside it. In other words:
- Checksums and error correction codes are encoded as part of the payload bitstream itself
- Delivery metadata (addresses, routing information, etc.) is also encoded within the payload
- The language is agnostic to the information carried—it only presents the data with an aesthetic wrapper that is effectively ignored
What This Means
Glossia's role is to encode bits into natural language and decode them back to bits. It doesn't care what those bits represent:
- The bits could be raw data, checksums, error correction codes, or metadata
- All of these are treated equally as part of the payload bitstream
- The language layer is purely an aesthetic wrapper—converting bits to readable text and back, effectively ignored during decoding
Practical Implications
- Design your protocol: Include checksums, error correction, and metadata as part of the data you encode
- Glossia handles encoding/decoding: The language layer ensures the bits can be represented as natural text
- Your protocol handles integrity: Checksums and error correction verify and correct the decoded bits
- Separation of concerns: Glossia is transport-agnostic; your application layer handles semantics and integrity
This design ensures that Glossia remains a pure encoding layer—it faithfully represents whatever bits you give it, without making assumptions about what those bits mean or how they should be validated.
Why Wordlists Must Be Append-Only
Critical constraint: Both payload and cover wordlists are append-only.
What This Means
- ✅ You can add new words to the end of wordlists
- ❌ You cannot replace existing words
- ❌ You cannot reorder words in the wordlist
Why This Constraint Exists
Glossia encodes data by mapping words to their position (index) in the wordlist. When you encode a message:
- Data bits are converted to word indices
- Those indices are used to select words from the wordlist
- The selected words are embedded in generated sentences
When decoding:
- Payload words are extracted from the text
- Each word is looked up in the wordlist to find its index
- Those indices are converted back to data bits
If you change the wordlist order or replace words:
- Previously encoded messages would decode incorrectly
- The same index would point to a different word
- The decoded data would be corrupted
By keeping wordlists append-only:
- All existing indices remain valid
- Previously encoded messages can still be decoded correctly
- New words can be added without breaking backward compatibility
- The information contained in bits written before new words were added is preserved
Practical Implications
- When creating a wordlist, plan carefully - you can't fix mistakes by reordering
- If you need to add words, append them to the end
- If you need to remove a word, you can mark it as deprecated but shouldn't remove it (or you'll break backward compatibility)
- Wordlist versioning can help track when words were added, but the core list must remain stable
Merkle Mode
Glossia supports a Merkle mode (--merkle) that wraps payload words in a Merkle proof structure. This expands N payload words into 2N-1 total words (N payload words + N-1 Merkle nodes).
How Merkle Mode Works
- Tree Construction: Payload words become leaves of a binary Merkle tree
- Cover Word Assignment: The first N-1 cover words from the cover wordlist are used as internal nodes (Merkle nodes)
- Pre-order Traversal: The tree is traversed in pre-order to generate the final sequence
- Root Position: The most frequent cover word (typically "the") becomes the Merkle root and appears first in the sequence
Why Frequency Ordering Matters
Critical requirement: Cover words must be ordered by frequency (most frequent first) in the cover wordlist.
When Merkle mode is enabled:
- The first N-1 cover words are selected as Merkle tree internal nodes
- These words appear in the generated text as Merkle proof nodes
- The first cover word (most frequent) becomes the Merkle root and appears at the start of the sequence
By ordering cover words by frequency:
- Common words like "the", "a", "is" appear as Merkle roots and internal nodes
- This makes the generated text look more natural
- The Merkle proof structure is hidden within common, everyday words
Example: If "the" is the first word in your cover wordlist (most frequent), it will appear as the Merkle root when using --merkle mode, making the output look like natural prose starting with "The ..."
Decoding Merkle Mode
To decode a Merkle-encoded message:
- Use
--demerklemode to parse the sequence - The parser extracts payload words by identifying which words are leaves (payload) vs internal nodes (cover words)
- The original payload sequence is recovered in order
The Merkle structure provides cryptographic proof that the payload words are authentic and in the correct order, while maintaining the natural appearance of the generated text.
Prose Quality (Semantic Planning)
By default, Glossia embeds payload words into any grammatically valid slot. That is always correct, but it can read oddly — "the clock discovers the mountain" is grammatical yet nonsensical. Semantic planning biases word choice toward coherent pairings (an animate subject for a thinking verb, a place for a motion verb, and so on), so English output reads more naturally.
The guarantee: it never changes what you decode
Semantic planning only affects which cover words appear and how sentences are shaped. It never changes which payload words are emitted or their order. So an encoding decodes to exactly the same data whether or not this layer is active — decoding is just filtering the text against the payload wordlist, and the payload is untouched. You can always recover your data.
It's on by default for English. Languages without a semantic dataset are unaffected and behave exactly as before.
Prose quality (best-of-N)
The strongest quality lever is best-of-N: Glossia generates several realizations of the same payload and keeps the best-reading one. Selection is density-first — it never sacrifices how compactly your data is packed — and breaks ties on coherence. Because every candidate encodes the identical payload in order, choosing among them is free: the result always decodes the same.
In the web composer
The compose page has a Prose quality stepper (under the advanced options,
next to the variant controls). Higher = more candidates sampled = better-reading
prose, for the same message. 1 is the fastest (a single realization).
It composes with the Cover variant stepper cleanly:
- Cover variant picks which family of wordings you're looking at.
- Prose quality decides how hard to search that family for the best one.
Each cover variant is anchored to its own well-separated block of candidate seeds, so stepping the variant always gives a genuinely different wording, and changing the quality never makes the variant jump somewhere unrelated — at any sample size.
On the command line
# Sample 8 candidates, keep the densest / most coherent
glossia --dialect english-body --best-of 8 <your words...>
--best-of 1 (the default) is the classic single-pass behavior.
From the library / WASM
- Rust:
encode_into_language_best_of(...), orgenerate_text_best_of(...). - WASM:
encode_best_of(...)andencode_raw_base_n_best_of(...).
All take an N (candidate count); N = 1 falls back to a single encoding.
What "coherent" means here
Each noun in the wordlist carries a coarse class — animate (people, animals),
agentive (machines that can act, like an engine or sensor), thing, place,
or abstract. Each verb declares what it expects of its subject and object. When
Glossia fills a slot next to a payload verb, it prefers a cover word whose class
fits — so you get "the author discovers the theory" rather than "the clock
discovers the mountain."
This is a soft preference, never a hard rule: a forced payload word is always placed, even if no perfectly coherent frame exists. Coherence improves the prose; it never blocks your data.
Turning it off
Set the environment variable GLOSSIA_DISABLE_SEMANTICS=1 to force the classic
part-of-speech-only planning (no semantic biasing, no coherence scoring). Useful
for reproducing pre-semantic output or benchmarking.
Performance
Best-of-N is cheap. Generating a sentence is fast once Glossia has built its grammar tables — and that build happens once per process and is reused, so extra candidates cost only a fraction of a second each. In the web app the tables are built once when the page loads, so stepping the Prose quality control stays responsive.
Scope and accuracy
Semantic planning currently ships for English only. The word classifications
are generated with WordNet assistance and hand-tuned overrides; they're good but
not perfect, and because the layer is soft, an occasional mis-classification only
slightly nudges word choice — it never affects correctness or decoding. See
experiments/semantic_planner/ in the repository for the dataset and the tools
that regenerate it.
Grammar
Glossia uses a two-layer grammar system: a context-free grammar (CFG) layer that generates POS tag sequences, and a type-driven layer based on Montague Grammar that constrains POS slots via lambda calculus.
Grammar Files
Grammars are defined in YAML files located in languages/{language}/grammar.yaml. Each grammar file specifies:
- Types: Montague Grammar semantic types for each POS tag
- Rules: CFG production rules with weights and lambda terms
Example Grammar (English)
grammar:
name: "English Grammar"
types:
- name: "N"
lambda_type: "e -> t"
- name: "V"
lambda_type: "e -> (e -> t)"
- name: "Adj"
lambda_type: "e -> e"
- name: "Det"
lambda_type: "(e -> t) -> (e -> t)"
- name: "Cop"
lambda_type: "(e -> t) -> (e -> t)"
- name: "Dot"
lambda_type: "t"
rules:
sentence:
lambda: "N"
cfg_productions:
- production: "NP VP Dot"
weight: 0.85
lambda: "N"
- production: "NP Adv V NP Dot"
weight: 0.10
lambda: "N"
- production: "NP V NP Dot"
weight: 0.05
lambda: "N"
noun_phrase:
lambda: "N"
cfg_productions:
- production: "N"
weight: 1.0
lambda: "N"
- production: "Det[def] N"
weight: 0.50
lambda: "Det"
- production: "Det[indef] N"
weight: 0.25
lambda: "Det"
verb_phrase:
lambda: "N"
cfg_productions:
- production: "VP_BASE"
weight: 0.70
lambda: "N"
- production: "VP_BASE VP_PP_TAIL"
weight: 0.30
lambda: "N"
Key Syntax
- Terminal symbols are POS tags:
Det,Adj,N,V,Modal,Aux,Cop,To,Prep,Adv,Conj,Dot,Prefix - Refinement tags in brackets constrain cover word selection:
Det[def]selects definite determiners ("the"),Det[indef]selects indefinite ("a", "an"),Det[quant]selects quantifiers ("each", "some") - Weights determine probabilistic selection between alternative productions
- Rule names map to standard abbreviations:
noun_phrase->NP,verb_phrase->VP,sentence->S
POS Tags
Glossia uses 13 parts of speech:
| POS | Description | Example Words |
|---|---|---|
Det | Determiner | the, a, each, some |
Adj | Adjective | big, red, fast |
N | Noun | cat, house, time |
V | Verb | run, see, get |
Modal | Modal verb | may, can, might |
Aux | Auxiliary verb | has, will, does |
Cop | Copula | is, are, was |
To | Infinitive marker | to |
Prep | Preposition | in, on, for, to |
Adv | Adverb | very, already, out |
Conj | Conjunction | and, but, or |
Dot | Sentence terminator | . |
Prefix | Sentence prefix | Re:, Fwd: |
POS Slot Constraints
- Payload-eligible POS:
N,V,Adj,Adv,Prep,Det,Conj- these slots can be filled with either payload or cover words - Cover-only POS:
Aux,Cop,To,Prefix,Dot,Modal- these slots are always filled with cover (function) words
Montague Grammar Type System
Each POS tag maps to a semantic type from Montague Grammar:
| POS | Type | Interpretation |
|---|---|---|
| N | e -> t | Predicate over entities |
| V | e -> (e -> t) | Relation between entities |
| Adj | e -> e | Entity modifier |
| Adv | (e -> t) -> (e -> t) | Predicate modifier |
| Det | (e -> t) -> (e -> t) | Quantifier |
| Prep | e -> (e -> t) | Prepositional relation |
| Conj | t -> (t -> t) | Proposition connective |
| Cop | (e -> t) -> (e -> t) | Subject-predicate linker |
| Modal/Aux/To | (e -> t) -> (e -> t) | Predicate modifier |
| Dot | t | Sentence truth value |
| Prefix | t -> t | Sentence wrapper |
Where:
e= Entity typet= Truth value type->= Function type
These types ensure that constituents combine in semantically valid ways, beyond what the CFG alone can enforce.
Dialect Support
Grammar files can define dialect overlays that modify the base rules for different generation modes. Dialects are defined in a separate dialect.yaml file:
dialects:
subject:
rules:
sentence:
cfg_productions:
- production: "Prefix NP Cop Adj Dot"
weight: 0.30
- production: "NP Cop Adj Dot"
weight: 0.70
poetry:
rules:
sentence:
cfg_productions:
- production: "NP VP Dot"
weight: 0.50
- production: "Adj N V Adv Dot"
weight: 0.50
The --dialect subject and --dialect body flags select which dialect overlay to apply on top of the base grammar rules.
Sentence Length Strategy
The grammar interacts with the length mode to control output:
- Compact mode: Tries sentence lengths from
k_mintok_max, stopping at the first length where the payload fits. Within that length, picks the highest-probability POS sequence. - Natural mode: Samples sentence length from the grammar's probability distribution across all valid lengths. This produces more varied, natural-sounding text.
The scoring function for balancing compactness vs naturalness:
score = log P(sequence) - λ * length
Higher λ yields shorter text; lower λ yields more natural text.
Dialect Calculus
Glossia's type system and CCG combinators don't just compose words into sentences — they compose dialects into pipelines. This chapter develops the dialect calculus: a typed lambda calculus where dialects are types and encoding operations are terms.
Motivation
Glossia already supports translation between languages (see How It Works). The bitstream is the common intermediate representation:
Source Language Text → Binary Data (bits) → Target Language Text
But a "language" is actually three independent choices fused together:
| Axis | Examples | What it determines |
|---|---|---|
| Input format | hex (Nostr), base64 (PGP), raw bytes (BIP39) | How bits are packed into wordlist indices |
| Vocabulary | payload.yaml, payload_base58.yaml | Which words carry data (and bits per word) |
| Output grammar | latin/body, english/prose, cs/sig | Syntactic shape of the output |
Current dialects like cs/sig_nostr fuse all three: hex input, base16 vocabulary, and ASCII armor grammar. But you should be able to mix freely:
- Encode a Nostr signature (hex) as Latin prose
- Convert a PGP message from ASCII-armored base64 into English body text
- Transcode Latin prose into English prose directly
The dialect calculus makes this composition type-safe.
Dialects as types
Recall Glossia's Montague Grammar type system:
e— Entity typet— Truth type (a valid sentence / proposition)A -> B— Function typeRefined(r, A)— Refined type (typeAwith refinement tagr)
At the object level, e is an individual thing and t is a sentence about things. At the dialect level, we reinterpret:
e— a data representation (hex string, base64 string, Latin text, English text, raw bytes)t— a valid encoded output (data successfully encoded in some dialect)
The Refined type distinguishes dialect entities:
Refined("latin/body", e) -- Latin prose as data representation
Refined("english/body", e) -- English prose as data representation
Refined("hex", e) -- hex string
Refined("base64", e) -- base64 string
Refined("nostr/sig", e) -- Nostr Schnorr signature (refines hex)
Refined("pgp/msg", e) -- PGP message (refines base64)
Refined("bits", e) -- raw bitstream (universal intermediate)
POS tags as dialect operations
Every POS tag already has a Montague type. The dialect calculus reinterprets each type as an operation on data representations:
| POS | Type | Object-level meaning | Dialect-level meaning |
|---|---|---|---|
| N | e -> t | predicate over entities | Encoder: given a data representation, produce valid encoded text |
| V | e -> (e -> t) | relation between entities | Transcoder: given a source, return an encoder for a target |
| Adj | e -> e | entity modifier | Format transform: rewrite one representation as another (e.g., hex decode to bytes) |
| Det | (e -> t) -> (e -> t) | quantifier | Codec modifier: wrap an encoder with a DataMode (ascii7, hex, base64) |
| Adv | (e -> t) -> (e -> t) | predicate modifier | Pipeline modifier: wrap an encoder with logging, verification, etc. |
| Conj | t -> (t -> t) | proposition connective | Combiner: concatenate or interleave two encoded outputs |
| Prep | e -> (e -> t) | prepositional relation | Parameterized encoder: "encode X into Y", "decode X from Y" |
| Cop | (e -> t) -> (e -> t) | subject-predicate linker | Identity wrapper: "X is encoded as Y" |
| Dot | t | sentence truth value | Pipeline terminator: the complete, validated output |
CCG combinators as pipeline algebra
The five CCG combinators from lambda_terms.rs have direct dialect-level meanings:
B — Composition (transcode)
B f g x = f(g(x))
The B combinator is the fundamental transcoding operation. Given:
decode_latin : Refined("latin/body", e) -> Refined("bits", e)— anAdj(format transform)encode_english : Refined("bits", e) -> t— anN(encoder)
Their composition is:
B encode_english decode_latin : Refined("latin/body", e) -> t
This says: "decode Latin prose to bits, then encode as English prose." It's the same B N Adj composition used at the object level for "the big (Adj) house (N)" — compose a modifier with a predicate.
Type checking confirms it:
B : (b -> c) -> (a -> b) -> a -> c
encode_english : Bits -> t (b = Bits, c = t)
decode_latin : LatinText -> Bits (a = LatinText, b = Bits)
─────────────────────────────────────────
B encode_english decode_latin : LatinText -> t ✓
C — Flip (reverse direction)
C f x y = f(y)(x)
If transcode has type Source -> Target -> t, then C transcode flips the arguments:
transcode : Source -> (Target -> t)
C transcode : Target -> (Source -> t)
This converts "transcode from Latin into English" to "transcode into English from Latin" — the same pipeline, different argument order. The C combinator handles word-order variation at the object level (SOV vs. SVO); at the dialect level, it handles directionality.
T — Type raising (lift data to pipeline)
T x f = f(x)
Given concrete data nostr_sig : e (a specific Nostr signature), type raising produces:
T nostr_sig : (e -> t) -> t
This lifts a specific piece of data into a function that accepts any encoder and applies it. "Here is a Nostr signature — give me any encoder and I'll feed the data through it."
At the object level, T raises a noun phrase to accept any verb phrase. At the dialect level, it raises data to accept any encoding pipeline.
S — Distribution (fork)
S f g x = f(x)(g(x))
The S combinator applies two functions to the same input, then combines:
S transcode_english transcode_latin data = transcode_english(data)(transcode_latin(data))
This encodes the same data in two formats simultaneously — useful for generating both a Latin and English version from the same bitstream.
I — Identity (passthrough)
I x = x
The identity encoder: data passes through unchanged. Useful as a default when no transformation is needed.
Worked example: Nostr signature in Latin
"Encode a hex Nostr signature as Latin prose."
Step 1: Assign dialect types
nostr_sig : Refined("nostr/sig", e) -- the data (a Pron)
hex_decode : Refined("hex", e) -> Refined("bits", e) -- hex → bytes (an Adj)
encode_latin : Refined("bits", e) -> t -- bytes → Latin (an N)
hex_mode : (e -> t) -> (e -> t) -- DataMode::Hex (a Det)
Step 2: Build the pipeline
hex_mode(B encode_latin hex_decode)(nostr_sig) : t
Reading inside-out:
hex_decodetransforms the hex representation to bits (Adj : e -> e)B encode_latin hex_decodecomposes the Latin encoder after the decoder (B N Adj : e -> t)hex_mode(...)wraps the composed encoder with the hex DataMode (Det(N) : e -> t)- Apply to
nostr_sigto get the final output ((e -> t)(e) : t)
Step 3: Type checking
hex_mode : (e -> t) -> (e -> t) -- Det
B encode_latin hex_decode : e -> t -- composed N
hex_mode(B encode_latin hex_decode) : e -> t
nostr_sig : e
─────────────────────────────────
hex_mode(B encode_latin hex_decode)(nostr_sig) : t ✓
The type checker proves this pipeline is well-formed: the output is a valid encoding (t).
Worked example: Latin to English transcoding
"Read Latin prose and output English prose."
decode_latin : Refined("latin/body", e) -> Refined("bits", e) -- Adj
encode_english : Refined("bits", e) -> t -- N
transcode = B encode_english decode_latin
: Refined("latin/body", e) -> t
This is a single B N Adj application — the same combinator that composes adjectives with nouns in ordinary Montague Grammar.
Worked example: PGP message in English
"Convert a PGP base64 message into English body text."
pgp_msg : Refined("pgp/msg", e) -- Pron
base64_decode : Refined("base64", e) -> Refined("bits", e) -- Adj
encode_english : Refined("bits", e) -> t -- N
base64_mode : (e -> t) -> (e -> t) -- Det
base64_mode(B encode_english base64_decode)(pgp_msg) : t
The bitstream as zero object
Every dialect D that carries payload is isomorphic to the bitstream:
encode_D : Bits -> D
decode_D : D -> Bits
decode_D . encode_D = id_Bits
encode_D . decode_D = id_D
This means every morphism between dialects factors through bits:
hom(D1, D2) = {encode_D2 . decode_D1}
In category-theoretic terms, Bits is a zero object in the category of dialects — every dialect has exactly one morphism to and from it. The B combinator computes these factored morphisms.
This is why Glossia translation is always lossless and commutative (see Bijectivity):
A -> B -> C = A -> C
The dialect calculus makes this algebraic structure explicit.
DataMode as a natural transformation
The four DataModes (Bytes8, Ascii7, Base64, Hex) are not encoders themselves — they are natural transformations between encoding functors.
A DataMode m transforms any encoder enc : e -> t into a modified encoder m(enc) : e -> t that pre-processes the input. In the dialect calculus, this is exactly the Det type:
Det : (e -> t) -> (e -> t)
The naturality condition says: applying a DataMode and then transcoding gives the same result as transcoding and then applying the DataMode. This holds because DataModes operate on the bits, which are invariant under transcoding:
B (m(encode_B)) decode_A = m(B encode_B decode_A)
Connection to Curry-Howard
The Curry-Howard correspondence is the observation that types are propositions and programs are proofs. A function of type A -> B doesn't just transform data — it proves that A implies B. Type-checking a program is the same as verifying a proof. If the types don't line up, the "proof" is invalid — you've asserted something that doesn't follow.
This isn't a metaphor. Curry noticed in 1934 that combinator types match axiom schemes of intuitionistic logic. Howard made it precise in 1969: natural deduction proofs correspond exactly to typed lambda terms. Every logical connective has a computational counterpart — conjunction is pairing, disjunction is tagged union, implication is function abstraction. See Philip Wadler's Propositions as Types for an accessible treatment of the full story.
The dialect calculus inherits this correspondence directly. Each column below is the same formal object seen from three angles:
| Lambda calculus | Logic | Dialect calculus |
|---|---|---|
Type A -> B | Proposition "A implies B" | "Given data in format A, I can produce format B" |
Term f : A -> B | Proof of "A implies B" | A concrete encoder/decoder |
Application f(x) | Modus ponens | Running the pipeline on data |
Composition B f g | Transitivity of implication | Transcoding via intermediate representation |
t (truth) | Theorem | Valid encoded output exists |
| Type error | Contradiction | Incompatible pipeline (wrong wordlist, wrong DataMode) |
What this buys us
1. Type-checking is proof verification. When the dialect calculus type-checks a pipeline like
hex_mode(B encode_latin hex_decode)(nostr_sig) : t
it is simultaneously verifying a proof that "given a Nostr hex signature and a hex decoder and a Latin encoder, a valid Latin encoding exists." If any component has the wrong type — say you accidentally pass a Latin decoder where a hex decoder is expected — the proof fails. The type system catches the error before any data flows.
2. Composition is transitivity. The B combinator computes B f g x = f(g(x)). Logically, if g proves A => B and f proves B => C, then B f g proves A => C. This is why transcoding works: decode_latin proves "Latin implies Bits" and encode_english proves "Bits implies English", so their composition proves "Latin implies English." The proof factors through the bitstream, just as the data does.
3. Uninhabited types mean impossible pipelines. If no term of type Refined("hex", e) -> Refined("latin/body", e) exists (because there's no direct hex-to-Latin transform), then the corresponding proposition "hex implies Latin" has no proof. You must factor through Bits — and the type system forces you to make this explicit by composing B encode_latin hex_decode.
4. The zero object is the tautology. Bits being a zero object (every dialect has exactly one morphism to and from it) means that Bits => D and D => Bits are both provable for every dialect D. In logical terms, Bits is equivalent to every well-formed dialect — it's the canonical witness that all lossless encodings preserve the same information.
Meta-grammar
The meta-language for dialect pipelines uses the same CFG as the object language. A pipeline specification is a sentence in the meta-grammar:
S -> NP VP Dot
NP -> Det N "the hex encoder"
NP -> Adj N "the Latin decoder"
VP -> V NP "transcodes into English"
VP -> V PP "encoded from Latin"
PP -> Prep NP "from Latin" / "into English"
The semantic derivation of this meta-sentence IS the pipeline specification. The same type-driven grammar that validates "the cat sees the dog" also validates "the hex decoder transcodes into Latin prose."
This self-similarity — the meta-language having the same grammar as the object language — is not a coincidence. It follows from the fact that both levels use the same type algebra (e, t, ->, Refined). The grammar is a free algebra over these types; lifting the interpretation of e and t lifts the entire grammar.
Summary
| Concept | Object level (words → sentences) | Dialect level (formats → pipelines) |
|---|---|---|
e | Individual entity | Data in some representation |
t | Valid sentence | Valid encoded output |
N : e -> t | Noun (predicate) | Encoder |
V : e -> (e -> t) | Verb (relation) | Transcoder |
Adj : e -> e | Adjective (modifier) | Format transform (decode) |
Det : (e -> t) -> (e -> t) | Determiner (quantifier) | DataMode (codec modifier) |
Conj : t -> (t -> t) | Conjunction | Output combiner |
B f g | Compose modifier + predicate | Transcode (decode then encode) |
C f | Flip argument order | Reverse pipeline direction |
T x | Type-raise entity | Lift data to accept any encoder |
S f g | Distribute over argument | Fork: encode same data two ways |
Refined(r, A) | Language-specific feature | Dialect identity |
Language Support
Glossia supports multiple languages. Each language provides its own wordlists, grammar rules, and type definitions.
Supported Languages
| Language | Wordlists | Grammar | Status |
|---|---|---|---|
| English | BIP39, n-gram, WordNet lemmas | Full YAML grammar with dialects | Fully implemented |
| Latin | Custom (65,536 words), Harry Potter | Full YAML grammar with dialects | Fully implemented |
| Czech | BIP39 (official, 2,048 words) | Full YAML grammar (body/subject/prose) | Fully implemented |
| Math/Primes | Prime numbers (payload), composites (cover) | Arithmetic grammar | Experimental |
| Harry Potter | HP spell vocabulary | Custom | Experimental |
| French | BIP39 (text only) | Not yet | Planned |
| German | BIP39 (text only) | Not yet | Planned |
Using Languages
# Default: English with BIP39 wordlist
glossia --random 12 --seed 0
# English with n-gram wordlist
glossia --random 12 --wordlist ngram --seed 0
# Specify language explicitly
glossia --random 12 --language english --seed 0
# Czech (official BIP39 wordlist)
glossia --random 12 --language czech --seed 0
Language File Structure
Each language lives in languages/{language}/ and requires:
languages/my_language/
├── grammar.yaml # Grammar rules with Montague types
├── payload.yaml # Payload wordlist with POS weights
├── cover.yaml # Cover wordlist with POS weights
└── dialect.yaml # Optional: dialect overlays (subject/body/poetry)
In addition, the language must be registered in the shared meta-grammar layer:
languages/meta/
├── payload.yaml # Append language name here as a dialect identifier
├── cover.yaml # Append new pipeline function words here (if needed)
└── grammar.yaml # Dialect calculus rules (usually no changes needed)
Wordlist Format
Wordlists are YAML files mapping words to POS tag weights:
abandon:
V: 0.6
N: 0.4
ability:
N: 1.0
able:
Adj: 1.0
Words can have multiple POS tags with weights that sum to 1.0. Optional refinement tags can constrain cover word selection:
the:
Det: 1.0
refinement: def
a:
Det: 1.0
refinement: indef
Naming Conventions
Multiple wordlists per language are supported:
payload.yaml/cover.yaml- default pairpayload_bip39.yaml/cover.yaml- BIP39 payload with general coverpayload_ngram.yaml/cover_ngram.yaml- n-gram based pairpayload_hp.yaml- Harry Potter themed payload
The build system automatically pairs payload_X.yaml with cover_X.yaml (or cover.yaml as fallback) for disjointness validation.
Compile-Time Validation
The build system (build.rs) performs several checks at compile time:
- Scans all
languages/directories for YAML files - Pairs payload and cover wordlists
- Validates disjointness - panics if any word appears in both payload and cover
- Embeds all YAML files as
include_str!()in the binary
In debug builds, only English is embedded for faster compilation.
Creating a New Language
See the Tools chapter for the complete workflow. The process has two layers: creating the language itself, and registering it in the meta-grammar so that pipeline instructions can reference it.
Language Files
- Creating payload and cover wordlists with POS tagging
- Writing grammar rules in YAML format
- Testing and validating your language
Meta-Grammar Registration
Every new language must also be registered in the meta-grammar (languages/meta/), which is Glossia's Dialect Calculus layer. This enables pipeline instructions like "translate from english into my_language".
- Append the language name to
languages/meta/payload.yamlwith appropriate POS weights (typicallyN,Adj,Pron). The meta payload must remain power-of-2 sized. - Check
languages/meta/cover.yamlfor any new pipeline function words your language may need (new verbs, prepositions, etc.). Append them if not already present. - Add a match arm in
src/pipeline.rsinmeta_word_to_language()to map the meta payload word to your language directory and default dialect.
Key Requirements
- Disjoint wordlists: Payload and cover words must never overlap (enforced at compile time)
- Append-only: Both wordlists are append-only for backward compatibility
- POS coverage: Cover wordlist must include words for all function-word POS tags (Aux, Cop, To, Prefix, Dot)
- Frequency ordering: Cover words should be ordered by frequency (most frequent first), especially for Merkle mode
- Meta-grammar registration: The language name must appear in
languages/meta/payload.yamlandsrc/pipeline.rsfor pipeline routing to work
English Wordlists
The English language includes several wordlist options:
| Wordlist | Size | Bits/Word | Use Case |
|---|---|---|---|
| BIP39 | 2,048 | 11 | Standard cryptocurrency seed phrases |
| N-gram | Large | Variable | General English text encoding |
| WordNet lemmas | Large | Variable | Comprehensive English vocabulary |
Latin Vocabulary
The Latin language provides 2^16 = 65,536 words (16 bits/word), enabling more compact encodings:
- A 12-word BIP39 seed phrase (132 bits) can be represented with just 9 Latin words (132 / 16 = 8.25, rounded up to 9)
- This demonstrates how larger wordlists enable more compact encodings
Designing Your Own Glossia
To construct a custom language for Glossia, you will need three essential components:
- A payload wordlist with weighted parts of speech
- A cover wordlist with weighted parts of speech
- A grammar file (YAML with CFG productions and Montague types)
The grammar must take any payload sequence and return a sequence of cover words plus grammar words that preserves the order of the payload. This ensures payload words appear in the same order in the generated text as in the input. The grammar is deterministic but may accept additional parameters (such as seeds) to guide output variety.
Important Considerations
- Decoding requirement: Others need to decode your messages! They only need the payload wordlist. This is why Glossia includes BIP39 wordlists by default.
- No overlap: The cover wordlist must never contain any words from the payload wordlist. This is enforced at compile time.
- POS weights: Words can have multiple parts of speech with different probabilities.
- Append-only wordlists: Both payload and cover wordlists are append-only:
- You can add new words to the end
- You cannot replace or reorder existing words
- Glossia encodes data by mapping words to their index in the wordlist; changing order breaks decoding
1. Payload Wordlist
The payload wordlist is a YAML file mapping words to their POS tag weights:
abandon:
V: 0.6
N: 0.4
ability:
N: 1.0
able:
Adj: 1.0
about:
Prep: 0.6
Adv: 0.4
Key points:
- Weights should sum to 1.0 per word (Glossia normalizes if needed)
- Valid POS tags:
N,V,Adj,Adv,Prep,Det,Modal,Aux,Cop,To,Conj - Frequency ordering: Custom payload wordlists should be ordered by frequency (most frequent first). BIP39 is frozen in alphabetical order.
2. Cover Wordlist
Same YAML format as the payload wordlist, but for words that fill non-payload slots:
the:
Det: 1.0
refinement: def
a:
Det: 1.0
refinement: indef
is:
Cop: 1.0
after:
Prep: 0.6
Adv: 0.4
Critical requirements:
- Must NOT overlap with the payload wordlist (compile-time enforced)
- Must include function words (determiners, prepositions, conjunctions, copulas, etc.)
- Optional
refinementtags enable grammar-driven morphology (e.g.,Det[def]selects "the",Det[indef]selects "a") - Frequency ordering: Order by frequency (most frequent first), especially for Merkle mode where the first cover word becomes the Merkle root
3. Grammar File
Grammar files are YAML with Montague Grammar types and CFG productions:
grammar:
name: "My Language"
types:
- name: "N"
lambda_type: "e -> t"
- name: "V"
lambda_type: "e -> (e -> t)"
- name: "Adj"
lambda_type: "e -> e"
- name: "Det"
lambda_type: "(e -> t) -> (e -> t)"
- name: "Cop"
lambda_type: "(e -> t) -> (e -> t)"
- name: "Dot"
lambda_type: "t"
rules:
sentence:
lambda: "N"
cfg_productions:
- production: "NP VP Dot"
weight: 0.85
lambda: "N"
- production: "NP V NP Dot"
weight: 0.15
lambda: "N"
noun_phrase:
lambda: "N"
cfg_productions:
- production: "N"
weight: 0.40
lambda: "N"
- production: "Det[def] N"
weight: 0.35
lambda: "Det"
- production: "Det[indef] N"
weight: 0.25
lambda: "Det"
verb_phrase:
lambda: "N"
cfg_productions:
- production: "V NP"
weight: 0.50
lambda: "N"
- production: "Cop Adj"
weight: 0.30
lambda: "N"
- production: "Modal V NP"
weight: 0.20
lambda: "N"
Key syntax:
- Terminal symbols are POS tags:
Det,Adj,N,V,Modal,Aux,Cop,To,Prep,Adv,Conj,Dot,Prefix - Refinement tags in brackets:
Det[def],Det[indef],Det[quant] - Weights determine probabilistic selection
- Rule names map to abbreviations:
noun_phrase->NP,verb_phrase->VP,sentence->S
Optional dialect overlay (dialect.yaml):
dialects:
subject:
rules:
sentence:
cfg_productions:
- production: "NP Cop Adj Dot"
weight: 1.0
Test your grammar:
glossia --show-grammar --dialect body
Complete Workflow
-
Create language directory:
mkdir -p languages/my_language -
Create payload wordlist:
- Start with your word list
- Tag with POS:
cargo run --bin tag_words -- -i words.txt -o words_POS.txt - Generate weights:
cargo run --bin validate_pos_weights -- --file words.yaml --output payload.yaml
-
Create cover wordlist:
- Generate common words:
cargo run --bin get_top_words -- -n 1000 --download-coca -o cover_words.txt - Tag with POS:
cargo run --bin tag_words -- -i cover_words.txt -o cover_POS.txt - Generate weights:
cargo run --bin validate_pos_weights -- --file cover.yaml --output cover.yaml - Overlap is validated automatically at compile time
- Generate common words:
-
Create grammar file:
- Write
grammar.yamlwith types and rules - Optionally add
dialect.yamlfor subject/body variants - Test with
glossia --show-grammar --dialect body
- Write
-
Register the language in the meta-grammar:
The meta-grammar (
languages/meta/) is Glossia's dialect calculus layer. It treats language and format names as payload words so that pipeline instructions like"translate from english into my_language"can be parsed. When you add a new language, you must register it in the meta-layer.-
Append to
languages/meta/payload.yaml: Add your language name with POS weights. Language names typically useN(encoder),Adj(format modifier), andPron(data reference):my_language: N: 0.5 Adj: 0.4 Pron: 0.1The meta payload wordlist must remain a power-of-2 size (it is currently 16 = 2^4). If appending your language would exceed the current power-of-2 boundary, you may need to add additional identifiers to reach the next power of 2, or coordinate with existing entries.
-
Check
languages/meta/cover.yaml: If your language introduces new pipeline verbs, prepositions, or other function words that are not already present in the meta cover wordlist, append them there. For example, if your language requires a new encoding verb or a direction preposition not already covered byencode,decode,translate,from,into, etc., add it to the appropriate section oflanguages/meta/cover.yaml. -
Update
src/pipeline.rs: The pipeline parser has match arms that map meta payload words to concrete language configurations. Add your language tometa_word_to_language():#![allow(unused)] fn main() { "my_language" => Some(("my_language", "body")), }If your language introduces new dialect modifiers, also update
meta_word_to_dialect().
-
-
Test your language:
glossia --random 12 --language my_language --seed 0 -
Test meta-language integration:
glossia --meta "translate from english into my_language" <<< "abandon ability able" -
Verify decoding:
- Extract all words from output
- Filter to only words in your payload wordlist
- Verify the original payload is preserved in order
File Structure
Your language directory should look like:
languages/my_language/
├── grammar.yaml # Grammar rules with Montague types
├── payload.yaml # Payload wordlist with POS weights
├── cover.yaml # Cover wordlist with POS weights
└── dialect.yaml # Optional: dialect overlays (subject/body/poetry)
In addition to the language directory, you must also update these shared files:
languages/meta/payload.yaml # Append your language name as a dialect identifier
languages/meta/cover.yaml # Append any new pipeline function words (if needed)
src/pipeline.rs # Add match arm in meta_word_to_language()
Tools
Glossia includes several utility tools for creating and validating language files.
Word Frequency Tool
Generate word lists from frequency data:
# Download and use COCA word frequency data (recommended)
cargo run --bin get_top_words -- -n 1000 --download-coca -o output.txt
# Re-running reuses the cached download (use --force-download to refresh)
cargo run --bin get_top_words -- -n 1000 --download-coca -o output.txt
# Use a local wordfrequency.info format file
cargo run --bin get_top_words -- -n 1000 --wordfreq lemmas_60k.txt -o output.txt
# Use a CSV frequency file
cargo run --bin get_top_words -- -n 1000 --csv word-freq.csv -o output.txt
The get_top_words tool:
- Downloads word frequency data from wordfrequency.info (COCA corpus)
- Caches the download locally (reuses for 7 days)
- Filters for words with 6 or fewer characters (no punctuation)
- Extracts POS tags when available
- Sorts by frequency and returns the top N most common words
POS Tagging Tool
Assign parts of speech to words using nlprule:
# Tag a word list (one word per line)
cargo run --bin tag_words -- -i input_words.txt -o output_POS.txt
# Use alternative (faster) tagging method
cargo run --bin tag_words -- -i input_words.txt -o output_POS.txt --alternative
POS Weight Generation Tool
Generate POS tag weights from nlprule analysis:
# Generate weights for cover words
cargo run --bin validate_pos_weights -- \
--file languages/english/cover.yaml \
--output cover_nlprule_weights.yaml
# Generate weights for payload words
cargo run --bin validate_pos_weights -- \
--file languages/english/payload.yaml \
--output payload_nlprule_weights.yaml
# Test with limited words (for faster testing)
cargo run --bin validate_pos_weights -- \
--file languages/english/cover.yaml \
--max-words 10 \
--output test_weights.yaml
# Adjust minimum weight threshold and decimal places
cargo run --bin validate_pos_weights -- \
--file languages/english/cover.yaml \
--threshold 0.05 \
--round 2 \
--output cover_weights.yaml
The weight generation tool:
- Tests each word in multiple sentence contexts using nlprule
- Calculates observed POS tag frequencies
- Normalizes weights to sum to 1.0
- Filters out weights below a threshold (default: 0.01)
- Rounds weights to specified decimal places (default: 3)
Weight Cleanup Tool
Remove zero-weighted productions from grammar files:
cargo run --bin cleanup_weights -- --file grammar.yaml --output grammar_clean.yaml
Weight Comparison Tool
Compare POS weight distributions between two wordlist files:
cargo run --bin compare_pos_weights -- \
--file1 payload_old.yaml \
--file2 payload_new.yaml
Workflow: Generate Short Words and Tag Them
A common workflow for building cover wordlists:
# Step 1: Generate shortest words (3-4 characters) from frequency data
cargo run --bin get_top_words -- \
-n 500 \
--download-coca \
--min-length 3 \
--max-length 4 \
--words-only \
-o shortest_words.txt
# Step 2: Tag those words with POS using nlprule
cargo run --bin tag_words -- \
-i shortest_words.txt \
-o shortest_words_POS.txt
Data Sources
The get_top_words tool uses word frequency data from:
- COCA (Corpus of Contemporary American English): Available from wordfrequency.info
- Google Books Ngram: Can process downloaded Ngram 1-gram files
Downloaded data is cached locally as lemmas_60k.txt and reused for 7 days before automatically refreshing.
Glossia Image Codec
A color-space codec that encodes arbitrary byte payloads into images. Each visual element (Voronoi cell, pixel, mosaic tile) carries information in its color, not its position. The codec operates natively in CIELAB perceptual color space.
Like the rest of Glossia, this is a readable encoding, not steganography: the payload is not hidden in the image, it is the image's colors. Anyone with the palette can decode it directly.
Core idea
A color palette defines a 1D curve $\gamma$ through CIELAB. At each palette point, a 2D constellation grid in the normal plane encodes additional bits. Every pixel color decomposes into:
- Tangential position on $\gamma$ → identifies the payload word (which palette color)
- Normal-plane displacement → encodes the sequence position (which occurrence of that word)
The rendering (Voronoi, grid, brush strokes, mosaic) is purely aesthetic. All information lives in the multiset of colors.
Architecture
palette.yaml Control points in CIELAB (viridis_approx, warm, cool)
|
v
PaletteCurve Cubic spline, arc-length reparameterized
|
v
BishopFrame Rotation-minimizing {T, U1, U2} via double-reflection
|
v
ConstellationMap Per-color M_i x M_i grids sized to local tube radius
|
v
encode / decode Payload words <-> CIELAB colors
|
v
RSEncoder Reed-Solomon byte-level error correction (optional)
|
v
render / capture Voronoi SVG, pixel grid, any visual form
Geometric components
Palette curve (PaletteCurve)
A cubic spline through K control points in CIELAB, reparameterized by arc length so that $|\gamma'(s)| \approx 1$. N palette colors are equally spaced along $\gamma$:
$$c_i = \gamma!\left(\frac{i \cdot L}{N-1}\right), \quad i = 0, \ldots, N-1$$
where $L$ is the total arc length.
For viridis_approx with N=16: L = 137.9, palette spacing = 9.2 CIELAB units.
Bishop frame (BishopFrame)
An orthonormal frame ${T(s), U_1(s), U_2(s)}$ propagated along $\gamma$ via the double-reflection method (Wang et al. 2008). Unlike the Frenet frame, the Bishop frame is smooth through inflection points and has no torsion-induced twist.
$U_1(0)$ is initialized to the Frenet normal at the curve start.
Tube radius (compute_tube_radius)
At each palette point, ray-march outward along $\pm U_1, \pm U_2$ (and intermediate angles) until the sRGB gamut boundary is hit. The minimum over all angles gives $r(s_i)$ -- the radius of the largest inscribed disk in the normal plane that stays within displayable colors.
For viridis_approx: $r$ ranges from 17.8 (bright end) to 59.5 (dark end).
Constellation (Constellation, ConstellationMap)
At palette point $c_i$, an $M_i \times M_i$ grid in $\text{span}{U_1(s_i), U_2(s_i)}$:
$$\text{point}(a, b) = c_i + \alpha_a \cdot U_1(s_i) + \alpha_b \cdot U_2(s_i)$$
$$\alpha_a = \left(a - \frac{M_i - 1}{2}\right) \cdot \varepsilon, \quad a = 0, \ldots, M_i - 1$$
where $M_i = \lfloor 2 r(s_i) / \varepsilon \rfloor + 1$ and $\varepsilon$ is the grid spacing (default: 2.3 CIELAB, the just-noticeable difference).
Each grid point encodes a sequence position $j = a \cdot M_i + b$, giving $M_i^2$ positions per palette color. The ConstellationMap holds one Constellation per palette color, exploiting the variable tube radius so fat-tube regions carry more data.
Bits per cell
$$\text{bits/cell} = \log_2 N + 2 \log_2 M_{\min}$$
| $\varepsilon$ | $M_{\min}$ | bits/cell (N=16) | $\sigma_{95}$ single pixel |
|---|---|---|---|
| 2.3 | 16 | 12 | 0.4 |
| 5.0 | 7 | 10 | 0.9 |
| 10.0 | 4 | 8 | 1.8 |
| 15.0 | 3 | 7 | 2.7 |
Encoding / decoding
Encode
For the $i$-th payload word with value $w$:
- Look up palette point: $s_w = w \cdot L / (N-1)$
- Get base color: $c_w = \gamma(s_w)$
- Assign sequence position $j$ (the $j$-th occurrence of word $w$)
- Map to displacement: $(a, b) = (j \mathbin{/} M_w,; j \bmod M_w)$, then $(\alpha_1, \alpha_2)$
- Pixel color: $c_w + \alpha_1 U_1(s_w) + \alpha_2 U_2(s_w)$
Decode (geometric)
For each pixel color $x$ in CIELAB:
- Project onto $\gamma$: find $s^* = \arg\min_s |x - \gamma(s)|$
- Snap to palette: $w = \text{round}(s^* \cdot (N-1) / L)$
- Decompose residual: $\delta = x - c_w$, then $\alpha_1 = \delta \cdot U_1$, $\alpha_2 = \delta \cdot U_2$
- Snap to grid: $(a, b) = \text{round}(\alpha / \varepsilon + (M-1)/2)$
- Recover position: $j = a M + b$
Noise model and the $\varepsilon / \sigma$ relationship
The grid spacing $\varepsilon$ sets the decision boundary at $\varepsilon / 2$. For a cell to decode correctly, the noise must not push the color past this boundary in either normal-plane axis.
For 2D Gaussian noise with per-axis $\sigma$:
$$P(\text{correct cell}) = \left[\text{erf}!\left(\frac{\varepsilon}{2\sqrt{2},\sigma}\right)\right]^2$$
For 99% cell accuracy: $\varepsilon \geq 5.6 \cdot \sigma$.
Spatial averaging
When a visual element (Voronoi cell, tile) spans $P$ screen pixels, the decoder can average all pixels in the region to reduce noise:
$$\sigma_{\text{eff}} = \frac{\sigma_{\text{raw}}}{\sqrt{P}}$$
$$\sigma_{95,\text{eff}} = \frac{\varepsilon}{5.6} \cdot \sqrt{P}$$
| Image size | Cells | px/cell | $\sqrt{P}$ | $\sigma_{95,\text{eff}}$ ($\varepsilon=2.3$) | $\sigma_{95,\text{eff}}$ ($\varepsilon=15$) |
|---|---|---|---|---|---|
| 100x100 | 64 | 156 | 12 | 5.1 | 33.5 |
| 400x400 | 64 | 2500 | 50 | 20.5 | 133.9 |
| 800x800 | 64 | 10000 | 100 | 41.1 | 267.9 |
QR code reference: $\sigma_{95} \approx 18$ (B/W threshold at $\Delta L^* \approx 50$, 4 px/module).
At 400x400 with $\varepsilon = 2.3$, we exceed QR's noise tolerance while using 10-20x fewer visual elements.
Reed-Solomon error correction (RSEncoder)
Wraps the parametric encoder with RS codes over GF(256) for fair comparison against QR codes (which use RS internally).
payload bytes -> RS encode (+ parity) -> bitstream -> pack into cells
cells -> unpack -> RS decode (correct errors) -> payload bytes
Bits are packed into cells using the minimum constellation size ($M_{\min}$) for uniform bit packing:
- word_bits = $\log_2 N$
- pos_bits = $2 \log_2 M_{\min}$
- bits/cell = word_bits + pos_bits
The joint decoder searches all $N$ palette colors per cell to find the $(w, j)$ pair with minimum residual. This is necessary because large constellation displacements can push a pixel closer to an adjacent palette color's base than to its own.
32-byte Nostr pubkey: Voronoi vs QR
| Config | Elements | bits/cell | $\sigma_{95,\text{eff}}$ (400x400) |
|---|---|---|---|
| QR-L V2 (25x25) | 625 modules | 1 | 18 |
| Voronoi, N=16, $\varepsilon$=2.3, 50% ECC | 32 cells | 12 | 110 |
| Voronoi, N=16, $\varepsilon$=15, 50% ECC | 64 cells | 7 | 134 |
| Voronoi, N=16, $\varepsilon$=30, 50% ECC | 64 cells | 6 | 268 |
Topological decoders (spectral_decoder.py)
Two rendering-agnostic decoders that use only color values, no spatial coordinates.
Rips filtration decoder
Implements 0-dimensional persistent homology on colors projected onto $\gamma$:
- Project each color onto $\gamma$ → arc-length value $s_i$
- Build Vietoris-Rips filtration: grow connection radius, track $\beta_0$ (connected components)
- Persistence gap separates within-word merges ($\varepsilon \approx 0$) from between-word merges ($\varepsilon \approx$ palette spacing)
- Cut at the gap → connected components = palette word groups
- Hungarian matching maps components to word indices
For clean data: persistence gap = 9.194 (exactly the palette spacing), 16 components detected, perfect round-trip.
The persistence gap is the topological analogue of the eigengap in spectral clustering. Both detect the natural cluster scale, but persistence does it via filtration (scanning all scales) rather than requiring a kernel bandwidth.
Spectral decoder
Normalized graph Laplacian on the 1D arc-length similarity graph:
- Project colors onto $\gamma$ → $s_i$ values
- Gaussian similarity: $W_{ij} = \exp(-|s_i - s_j|^2 / \sigma^2)$, with $\sigma = 0.4 \times$ palette spacing
- Normalized Laplacian: $L_{\text{sym}} = I - D^{-1/2} W D^{-1/2}$
- Eigengap determines $k$ (number of distinct words)
- K-means in spectral embedding → cluster assignments
In the continuous limit ($n \to \infty$), $L_{\text{sym}}$ converges to the weighted Laplace-Beltrami operator $\Delta_\rho$, whose eigenfunctions partition color space along low-density separators.
Why project onto $\gamma$ first?
At $\varepsilon = 2.3$, constellation displacements (up to 83 CIELAB) exceed palette spacing (9.2 CIELAB). Colors from adjacent palette words overlap in raw CIELAB. Clustering in 3D fails.
But the displacements are purely normal to $\gamma$ (by construction). Projecting onto $\gamma$ collapses the normal component, leaving only the tangential coordinate where palette words are well-separated. The projection is the key insight: it transforms a 3D overlapping problem into a 1D well-separated one.
Noise sweep results (per-pixel, no spatial averaging)
| $\sigma$ | Geometric | Rips | Spectral |
|---|---|---|---|
| 0 | 100% | 100% | 92.5% |
| 0.5 | 100% | 99.9% | 89.6% |
| 1.0 | 99.0% | 70.0% | 87.8% |
| 3.0 | 82.6% | 22.8% | 65.0% |
The geometric decoder wins per-pixel because it uses full 3D + constellation snapping. The Rips decoder is exact for clean data but fragile to noise (the 1D projection loses information). The spectral decoder is more noise-robust than Rips due to its global graph structure.
With spatial averaging, the topological decoders become dominant: averaging thousands of pixels per cell drives $\sigma_{\text{eff}} \to 0$, restoring the clean-data regime where Rips achieves 100%.
Rendering
The rendering is decoupled from encoding. Any spatial layout that assigns one color to each visual element works:
| Rendering | Spatial info needed? | Decoder |
|---|---|---|
| Voronoi diagram | seed positions | geometric or topological |
| Pixel grid | row-major order | geometric |
| Mosaic tiles | none | topological |
| Brush strokes | none | topological |
| Color-graded photo | none | topological + segmentation |
The Flask app (app.py) demonstrates Voronoi rendering with interactive controls for $\varepsilon$, palette, cell count, and environmental simulation (noise, brightness, color temperature, saturation).
Environment simulation
The app simulates four perturbation channels, all native to CIELAB:
| Channel | CIELAB operation | Physical source |
|---|---|---|
| Noise $\sigma$ | Gaussian on all 3 axes | Sensor noise, quantization |
| Brightness | $L^*$ offset | Exposure, ambient light |
| Color temp | $b^$ shift + $0.15 \cdot a^$ | Tungsten / shade / daylight |
| Saturation | Chroma scaling ($a^, b^$) | Display gamut, distance |
File reference
| File | Purpose |
|---|---|
parametric_encoding.py | Core library: curve, frame, constellation, encode/decode |
rs_encoding.py | Reed-Solomon wrapper, QR comparison, noise sweep |
spectral_decoder.py | Rips filtration and spectral decoders |
app.py | Flask demo with Voronoi rendering and environment simulation |
visualize_encoding.py | Voronoi seed generation and image rendering |
noise_analysis.py | Noise sweep and capacity frontier plots |
sweep_params.py | Parameter space sweep for optimal (N, $\varepsilon$, ECC) configs |
test_parametric.py | Round-trip, gamut, separation, noise, capacity tests |
palette.yaml | CIELAB control points for palette curves |
Key invariants
-
Constellation displacements are normal to $\gamma$: by construction, $\alpha_1 U_1 + \alpha_2 U_2 \perp T$. This ensures the curve projection recovers word identity exactly (clean data).
-
Tube radius bounds the constellation: $M_i = \lfloor 2r(s_i)/\varepsilon \rfloor + 1$ guarantees all constellation points are within the sRGB gamut.
-
Persistence gap = palette spacing: the Rips filtration's largest gap in death times equals $L/(N-1)$, the arc-length spacing between adjacent palette colors. This is a geometric invariant of the encoding, independent of the specific payload.
-
Spatial averaging multiplies noise tolerance by $\sqrt{P}$: a cell with $P$ pixels has effective noise $\sigma/\sqrt{P}$, giving $\sigma_{95,\text{eff}} = (\varepsilon/5.6) \cdot \sqrt{P}$.
-
Rendering is arbitrary: information lives in the multiset of colors, not spatial layout. The topological decoders formalize this via the Vietoris-Rips complex on color space.
Email Dialect
The email dialect (cs/email) models the anatomy of an RFC 5322 / MIME email message as a context-free grammar. Every part of the email is classified as either cover (structural) or payload (content) — nothing is ignored.
This dialect belongs under languages/cs/ because CS already models structured formats (ASCII armor for PGP, NIP-04, signatures). The email dialect adds email-specific structure following the same pattern: cover words for framing tokens, N slots for content zones.
Quick Start
# Encode data into a simple text/plain email
echo "attack at dawn" | glossia --into cs/base64/email
# Encode into multipart/alternative (text + html, like Gmail)
echo "attack at dawn" | glossia --into cs/base64/email_alt
# Encode into multipart/mixed (with attachment)
echo "attack at dawn" | glossia --into cs/base64/email_mime
Example output for cs/email:
From: <sender@glossia.local>
To: <recipient@glossia.local>
Date: Thu, 01 Jan 2026 00:00:00 +0000
Subject: qt2foq
Content-Type: text/plain; charset="UTF-8"
zp8tk5cwh4yrnx
The base64 payload characters (qt2foq...) carry the encoded data. Everything else — header labels, addresses, dates, MIME declarations — is cover.
Email Anatomy
Every part of an email maps to exactly one classification:
Return-Path: <alice@example.com> COVER (server-added envelope)
Received: from mail.example.com ... COVER (transport trace)
Date: Sun, 22 Feb 2026 19:40:58 -0600 COVER (stable envelope header)
From: Alice <alice@example.com> COVER (stable envelope header)
To: Bob <bob@gmail.com> COVER (stable envelope header)
Message-ID: <...@mail.example.com> COVER (server-generated)
Subject: Test message PAYLOAD ZONE 1
MIME-Version: 1.0 COVER (MIME infrastructure)
Content-Type: multipart/alternative; ... COVER (MIME container)
COVER (blank line separator)
--boundary COVER (MIME boundary)
Content-Type: text/plain; charset="UTF-8" COVER (part content-type)
COVER (blank line separator)
Hello Bob, ... PAYLOAD ZONE 2
--boundary COVER (next MIME part)
Content-Type: text/html; charset="UTF-8" COVER (HTML part header)
<html>...</html> COVER (structural duplicate)
--boundary-- COVER (closing boundary)
Classification principle: Server-mangled parts (Received, Return-Path, Message-ID, HTML duplicates) are cover — they're structural artifacts of the transport layer, not content the user created.
Dialects
Four dialect variants model increasing email complexity:
| Dialect | Structure | Use Case |
|---|---|---|
email | Envelope + Subject + text/plain body | Simple messages |
email_alt | + multipart/alternative (text + html) | Gmail-style messages |
email_mime | + multipart/mixed (text + attachment) | Messages with attachments |
email_raw | Body only, no structure | Testing / raw content |
email — Simple text/plain
sentence -> ENVELOPE_HEADERS SUBJECT_LINE PLAIN_PREAMBLE BODY
From: <sender@glossia.local>
To: <recipient@glossia.local>
Date: Thu, 01 Jan 2026 00:00:00 +0000
Subject: <payload zone 1>
Content-Type: text/plain; charset="UTF-8"
<payload zone 2>
email_alt — Multipart/alternative
sentence -> ENVELOPE_HEADERS SUBJECT_LINE MIME_ALT_PREAMBLE
MIME_TEXT_PART MIME_HTML_PART MIME_CLOSE
From: <sender@glossia.local>
To: <recipient@glossia.local>
Date: Thu, 01 Jan 2026 00:00:00 +0000
Subject: <payload>
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="glossia-alt"
--glossia-alt
Content-Type: text/plain; charset="UTF-8"
<payload>
--glossia-alt
Content-Type: text/html; charset="UTF-8"
<html><body></body></html>
--glossia-alt--
The HTML part is entirely cover — it's a structural duplicate of the text/plain payload.
email_mime — Multipart/mixed with attachment
sentence -> ENVELOPE_HEADERS SUBJECT_LINE MIME_PREAMBLE
MIME_TEXT_PART MIME_ATTACH MIME_CLOSE
Both the text/plain body and the attachment body carry payload. The attachment is wrapped with Content-Transfer-Encoding and Content-Disposition headers (all cover).
Grammar Design
The grammar uses the same POS-to-type mapping as all CS dialects, with structural passthrough lambdas (λn:(e->t). n). Email-specific non-terminal rules live in the email dialect's rules section and are inherited by child dialects via parent: email.
POS tag assignments
| Email element | POS | Refinement | Example |
|---|---|---|---|
| Header labels | Aux | from, to, subject, etc. | From:, Subject: |
| Content-Type lines | Prefix | text-plain, multipart-alt, etc. | Content-Type: text/plain; charset="UTF-8" |
| CTE / HTML body | Cop | cte-base64, html-body, etc. | Content-Transfer-Encoding: base64 |
| MIME infrastructure | Modal | mime-version, disp-attach | MIME-Version: 1.0 |
| Header values | To | from-val, to-val, date-val | <sender@glossia.local> |
| MIME boundaries | Dot | boundary, boundary-end, etc. | --glossia, --glossia-- |
| Line separator | Conj | — | \n |
| Payload characters | N | — | a, B, 7, /, + |
Header field values use the To POS (a forced-cover slot in the generator) because they are always cover — addresses, dates, and trace lines are structural, not payload.
Envelope headers
The grammar generates envelope headers with two weighted productions:
- 70%: Minimal headers (From, To, Date) — what a sent message looks like
- 30%: Full trace (Return-Path, Received, Date, From, To, Message-ID) — what a received message looks like
All header values are placeholders (<sender@glossia.local>, etc.) that application-layer tools replace with real values.
Cover Wordlist
The email cover wordlist (cover_email.yaml) contains 48 tokens across 7 POS categories. Every token contains characters outside the base64 alphabet (:, -, space, ", ;, ., \n) so the CS decoder can distinguish cover from payload.
Token categories:
- 11 header labels (Aux):
From:,To:,Subject:,Date:,Received:,Return-Path:,Message-ID:, etc. - 20 MIME type declarations (Prefix): text/plain, text/html, multipart/alternative, application/pdf, image/png, etc.
- 5 CTE + HTML (Cop): quoted-printable, base64, 7bit, 8bit, HTML body placeholder
- 3 MIME infrastructure (Modal): MIME-Version, Content-Disposition attachment/inline
- 6 header values (To): address placeholders, date placeholder, trace placeholder
- 6 MIME boundaries (Dot):
--glossia,--glossia--,--glossia-alt, etc. - 1 line separator (Conj):
\n
Integration with nostr-mail
The email dialect is designed as a reference grammar for tools that generate or parse email structure. A client like nostr-mail would use it at two levels:
1. Structure generation (grammar layer)
Use Glossia to encode data into email structure:
echo "$ENCRYPTED_PAYLOAD" | glossia --into cs/base64/email_alt --seed $SEED
This produces a structurally valid email with placeholder header values and the payload distributed across the subject line and body.
2. Header replacement (application layer)
Replace placeholder values with real ones via string substitution:
| Placeholder | Replace with |
|---|---|
<sender@glossia.local> | Sender's email/npub |
<recipient@glossia.local> | Recipient's email/npub |
Thu, 01 Jan 2026 00:00:00 +0000 | Actual send timestamp |
<msgid@glossia.local> | Generated Message-ID |
from glossia (localhost [127.0.0.1]) | Actual Received trace |
The grammar guarantees these placeholders appear exactly once per header field (via refinement tags like To[from-val], To[date-val]), so simple string replacement is safe.
3. Content composition (pipeline layer)
For richer content, compose the email container with a content encoding:
- Encode a mnemonic into English prose:
glossia --into english - Encode a short summary:
glossia --into english/default/subject - Use the email grammar as a template, inserting the prose into the body zone and the summary into the subject zone
This composition happens at the application layer — the email grammar describes where content zones go, not what goes in them.
Relationship to English Subject/Body
The email dialect and English subject/body dialects operate at different layers:
English subject / body | CS email | |
|---|---|---|
| Layer | Content (natural language prose) | Container (RFC 5322 structure) |
| Alphabet | BIP39 words (2048, space-delimited) | Base64 chars (64, concatenated) |
| Output | "Re: snake robot mixed ship" | Full email with headers and MIME |
| Payload slots | Grammar slots (N, V, Adj...) | Raw N slots in BODY rule |
They compose naturally: English generates the content, email provides the container.
Files
| File | Purpose |
|---|---|
languages/cs/grammar.yaml | Grammar rules and dialect definitions (email section) |
languages/cs/cover_email.yaml | Email structural tokens (48 cover words) |
languages/cs/payload_base64.yaml | Base64 character payload alphabet |
languages/cs/email_demo.py | Python demo script |
src/pipeline.rs | Pipeline routing ("email" keyword) |
Nostr Seal (ASCII Armor for Pubkeys)
The seal_nostr dialect wraps a bech32 public key inside ASCII armor so that nostr-mail emails have consistent, searchable markers in their plaintext part.
-----BEGIN NOSTR SEAL-----
npub17umm7nnvf6y2dse2gwyklhq0p9daeqzn6edp523fzfd5utj2upcsm6zk5r
-----END NOSTR SEAL-----
Why seals exist
Nostr-mail produces dual-format emails:
| Part | Format | Purpose |
|---|---|---|
text/plain | ASCII-armored blocks | Machine-readable; searchable by Gmail IMAP |
text/html | Glossia-encoded prose | Human-readable (Latin, English, etc.) |
The plaintext needs every block wrapped in armor so Gmail's X-GM-RAW search can find nostr-mail messages by searching for markers like BEGIN NOSTR SEAL or BEGIN NOSTR SIGNATURE.
Without the seal dialect, the pubkey would appear as a bare npub1... string with no searchable framing.
Encoding
The caller strips the npub1 prefix and passes only the bech32 data to glossia:
npub17umm7nnvf6y2dse2gwyklhq0p9daeqzn6edp523fzfd5utj2upcsm6zk5r
└──────────────── bech32 data (payload) ──────────────────┘
The npub1 prefix is a cover word (Prefix[npub]) that the grammar injects automatically. The caller does not include it in the payload.
This follows the same convention as crypto/nostr, where npub1 is always a cover prefix.
Decoding
To decode the seal:
- Strip the header (
-----BEGIN NOSTR SEAL-----\n) and footer (\n-----END NOSTR SEAL-----) - Strip the
npub1cover prefix from the remaining line - The rest is bech32 payload data
- Prepend
npub1to reconstruct the full npub
In pseudocode:
lines = seal_block.strip().split("\n")
# lines[0] = "-----BEGIN NOSTR SEAL-----"
# lines[1] = "npub1<bech32data>"
# lines[2] = "-----END NOSTR SEAL-----"
npub_line = lines[1]
assert npub_line.startswith("npub1")
bech32_data = npub_line[len("npub1"):]
# Reconstruct
full_npub = "npub1" + bech32_data
Since the grammar uses payload_line_width: null, the npub is always on a single line (no line wrapping).
Display name (optional)
The email layer may insert a display name line between the header and the npub:
-----BEGIN NOSTR SEAL-----
@alice
npub17umm7nnvf6y2dse2gwyklhq0p9daeqzn6edp523fzfd5utj2upcsm6zk5r
-----END NOSTR SEAL-----
The @alice line is added by the nostr-mail client, not by glossia. It is presentation metadata for human readers. The decoder should skip any line that does not start with npub1.
Dual-format email structure
A complete nostr-mail message uses three ASCII-armored blocks in the plaintext part:
-----BEGIN NOSTR SEAL-----
npub17umm7nnvf6y2dse2gwyklhq0p9daeqzn6edp523fzfd5utj2upcsm6zk5r
-----END NOSTR SEAL-----
-----BEGIN NOSTR ENCRYPTED MESSAGE-----
<NIP-44 ciphertext in base58, line-wrapped at 76 chars>
-----END NOSTR ENCRYPTED MESSAGE-----
-----BEGIN NOSTR SIGNATURE-----
c8ca1f5e59bc5e915a8044beff139359...
-----END NOSTR SIGNATURE-----
Each block maps to a glossia dialect:
| Block | Dialect | Payload alphabet | Line wrap |
|---|---|---|---|
| Seal (pubkey) | cs/seal_nostr | bech32 (32 chars) | No |
| Encrypted message | cs/nip44 | base58 (58 chars) | 76 chars |
| Signature | cs/sig_nostr | base16 / hex (16 chars) | 76 chars |
Grammar structure
The seal_nostr dialect overrides the base CS grammar's sentence production to inject the npub1 prefix between the header and the payload body:
sentence -> HEADER SEAL_PREFIX BODY FOOTER
HEADER -> Dot Aux[begin] Prefix[nostr] Modal[seal] Dot Conj
SEAL_PREFIX -> Prefix[npub]
BODY -> N | N BODY
FOOTER -> Conj Dot Aux[end] Prefix[nostr] Modal[seal] Dot
Token mapping:
| Token | Cover word | POS |
|---|---|---|
Dot | ----- | Dot |
Aux[begin] | BEGIN | Aux |
Aux[end] | END | Aux |
Prefix[nostr] | NOSTR | Prefix |
Modal[seal] | SEAL | Modal |
Prefix[npub] | npub1 | Prefix |
Conj | \n | Conj |
N | bech32 char | N (payload) |
All tokens except N are cover words. The minimum sequence length is 14 (6 header + 1 prefix + 1 body + 6 footer).
Pipeline usage
The seal keyword is a dialect modifier in the meta sentence parser. Combined with the nostr language keyword, it selects the cs/seal_nostr dialect:
transcode(bech32Data, "encode into seal nostr")
Where bech32Data is the bech32 character data after the npub1 prefix.
Gmail IMAP search
The ASCII armor markers enable Gmail search via X-GM-RAW:
# Find all nostr-mail messages
X-GM-RAW "BEGIN NOSTR"
# Find messages with seals specifically
X-GM-RAW "BEGIN NOSTR SEAL"
# Find messages with signatures
X-GM-RAW "BEGIN NOSTR SIGNATURE"
This works because the armor lines appear verbatim in the text/plain part of the email, which Gmail indexes for full-text search.
Files
| File | Purpose |
|---|---|
languages/cs/grammar.yaml | seal_nostr dialect definition |
languages/cs/cover.yaml | SEAL, npub1, and other cover words |
languages/cs/payload_bech32.yaml | 32-character bech32 payload alphabet |
src/pipeline.rs | "seal" dialect modifier routing |
src/wasm.rs | "Nostr Seal" display name |
Bulletin Board
A Glossia bulletin is an encrypted message published as natural-language
prose to a nostr identity (a random key, or one derived from
a passphrase). The site itself stays static: anyone opens a board by putting its
#npub in the URL, and the browser loads that board's bulletins straight from
public relays.
It is the project's core idea applied to messaging — machine data made human-friendly. A bulletin reads as prose, transcribes by voice, and verifies by eye, while every payload word still carries its full entropy. Nothing is hidden; the encryption is what protects the contents, not the encoding.
Read a board at /bulletin.html (open a
shared #npub link and add a read key, nsec, or passphrase to decrypt); post one at
/compose.html.
The model
A single signing key roots a three-tier capability hierarchy — so you never set a separate encryption password:
signing key d ──schnorr──────────────▶ npub (publish + identity)
──SHA256("glossia/content-key/v1" ‖ d)──▶ read key K (decrypt)
npub = d·G (locate + verify)
| Credential | Can publish? | Can decrypt? | Notes |
|---|---|---|---|
nsec (the signing key d) | ✅ | ✅ | can compute K = H(d) — full access |
read key (K, shared as nread1…) | ❌ | ✅ | one-way hash → can't recover d → can't sign |
| npub (public address, in the URL) | ❌ | ❌ | reads the prose and verifies signatures only |
The content key is derived one-way from the signing key, with domain separation. So:
- Share the nsec → co-authors can post and read.
- Share the read key → subscribers can read but not post or impersonate the board (broadcast / newsletter).
- Share nothing → anyone with the npub still reads the encrypted prose and can verify who signed it, but can't decrypt. (Publish unencrypted for a plain verifiable public feed.)
This subsumes the old single-key / two-key split into one root. Publishing leaks
neither d nor K: the schnorr signature and the AES-256-GCM ciphertext reveal
nothing about either.
Where the signing key comes from
- Random — a fresh full-entropy key; save the
nsecto post again. - Passphrase —
dis derived deterministically (PBKDF2, fixed domain salt) so the same passphrase always reaches the same board. Sharing the passphrase grants full access; the view page also accepts it to decrypt. - Bring your own
nsec— keep posting to an existing board.
Saving and loading a board
The compose page's address bar already is the board (the author link carries the
nwrite), so bookmarking it is the quickest way to return. For a durable backup
that survives a lost bookmark, Save board renders the board's signing key as a
Glossia seed phrase — the private key itself, written as a readable
paragraph of prose:
Save board ─▶ 32-byte signing key ‖ 4-byte checksum
─▶ encode_raw_base_n ─▶ natural-language paragraph ("write this down")
Load board ─▶ paste the paragraph ─▶ decode + verify checksum ─▶ board restored
This is the project's core idea applied to a private key: the seed phrase is the
key, made human-friendly — readable, speakable, transcribable. A 4-byte checksum
(sha256(key)[..4]) rides with the key, so a mistyped or garbled word is caught on
load instead of silently restoring a different board (the same safeguard BIP39
mnemonics use). The phrase is rendered in whichever prose language the board uses;
Load board auto-detects the language and the checksum confirms the decode. The
key is generated and encoded entirely in the browser — it never leaves the device.
Anyone who holds the seed phrase has the signing key, so it grants full access
(post and decrypt): treat it exactly like the nwrite.
How a bulletin is built
The message is compressed and encrypted with authenticated AES-256-GCM.
Per message, the AES key and nonce are derived from the board's content key K
and a fresh 6-byte random salt via HKDF-SHA-256 (the content key is already
high-entropy, so no PBKDF2 stretching is needed — decryption is instant). The
result is Glossia-encoded into prose. An encrypted
bulletin reads as a quote with an attribution: the prose is the ciphertext,
and the em-dash trailer carries the plumbing — [flag:2b | length:14b][salt:6][tag:12]
(20 bytes) — rendered as ~11 Latin payload words, so it scans like a cited source:
"Ara belle arbustum. Obatratus emptor perrogatio…" — Gelu Synapium Cenit Tolerantia Parium Remex …
└──────────────── ciphertext ───────────────────┘ └──── salt + 96-bit GCM tag ─────────────────┘
The reduction flag rides in the top 2 bits of the length field, so no version byte is needed — the em-dash alone signals the format, and it never appears in encoded prose, so the two halves split cleanly. Latin's ~15 bits/word keeps the trailer at 11 words. That artifact string is the body of a NIP-01 event:
kind: 1 (a standard text note — the prose is public and readable,
so it shows in any nostr client, like the encoding intends)
content: "<prose> — <attribution>" (encrypted) or "<prose>" (signed but unencrypted)
tags: [["client","glossia"], ["subject", "..."]]
sig: schnorr (BIP-340) signature over the event id, by the publish key
Publishing as kind 1 (rather than the app-specific kind 1314 boards used
originally) keeps a board legible in the wider nostr ecosystem: the prose is
meant to be seen, and only the payload inside it is encrypted. The event is
signed in the browser and pushed to several public relays. Reading a board is a
relay query for { authors: [pubkey], kinds: [1, 1314] } (kind 1314 is still
read so pre-existing boards keep loading); each event's
signature is verified before its prose is shown, and the message is revealed only
if you supply a decryption credential (read key / nsec / passphrase) — the GCM tag
then guarantees it decrypted to exactly what was published.
All crypto runs client-side. The schnorr/secp256k1 and SHA-256 primitives are the
audited @noble ESM builds, vendored
under web/vendor/noble so the page stays self-contained and offline-capable;
bech32 (npub / nsec / nread) is implemented to BIP-173 and cross-checked
against nostr-tools.
Security notes
- A board is public. Its npub, ciphertext-prose, post times, and subject tags are all visible to anyone. Encryption protects the message, not the fact that a board exists or how often it is posted to.
- Random boards have full-entropy keys. The signing key (and thus the read key) is random, so the content key is never guessable — there is no password to brute-force. Passphrase-derived boards are the exception: the npub is public, so a weak passphrase is open to offline guessing (recovering it yields both the signing key and the read key). The 200k-round PBKDF2 stretch raises the cost; use a strong, generated passphrase, or just use a random board.
- The read key only grants reading. It is a one-way hash of the signing key, so sharing it for read access can never leak the ability to post. Revoking a reader, though, means rotating to a new board (the key is static per board).
- Encryption is authenticated. AES-256-GCM gives confidentiality and integrity: a wrong key/passphrase or any tampering with the prose or the attribution fails cleanly instead of yielding garbage. The nostr signature independently authenticates the event, so you also always know which npub posted it.
- Relays are untrusted infrastructure. They can drop or withhold events and see all public metadata. Publishing to several relays adds redundancy; it adds no confidentiality.
Glossia remains a readable encoding, not steganography: the goal is to make encrypted machine data human-friendly, not to conceal that it exists.
Project Structure
Directory Layout
The repository is a Cargo workspace. The root crate (glossia) is the
library; the glossia command-line binary lives in the glossia-cli member,
and developer tooling lives in the glossia-tools member. This keeps the
library (built as cdylib + rlib) from sharing a target name with the CLI
binary, which previously broke cargo test --release for downstream consumers
(cargo#6313).
glossia/
├── Cargo.toml # Workspace root + glossia library package
├── build.rs # Build script: language embedding & validation
├── CLAUDE.md # Project instructions for Claude Code agents
│
├── glossia-cli/ # `glossia` CLI binary (package: glossia-cli)
│ ├── Cargo.toml
│ └── src/main.rs # Main CLI entry point
│
├── glossia-tools/ # Developer tooling (package: glossia-tools)
│ ├── Cargo.toml
│ └── src/bin/
│ ├── get_top_words.rs # Word frequency analysis tool
│ ├── tag_words.rs # POS tagging using nlprule
│ ├── cleanup_weights.rs # Remove zero-weighted productions
│ ├── compare_pos_weights.rs # Compare weight distributions
│ └── validate_pos_weights.rs # Validate POS weight files
│
├── src/
│ ├── lib.rs # Library root: re-exports, GrammarChecker
│ ├── types.rs # POS enum (13 variants), Sym enum
│ ├── semantic_types.rs # Montague Grammar types: Entity, Truth, Function, Refined
│ ├── lambda_terms.rs # Lambda calculus AST: Variable, Constant, Application, Abstraction
│ ├── lambda_parser.rs # Pest-based parser for lambda expressions
│ ├── type_driven_grammar.rs # Load grammar.yaml, apply type constraints
│ ├── grammar.rs # CFG parsing from YAML, production rules, dialect overlay
│ ├── codec.rs # Binary codec: encode/decode arbitrary data to/from words
│ ├── merkle.rs # Merkle trees: WordlistTree, merkleize, parse, verify
│ │
│ ├── generator/ # Text generation engine
│ │ ├── mod.rs # Public API exports
│ │ ├── types.rs # PayloadTok, Lexicon, GenerationMode, SentenceLengthMode
│ │ ├── core.rs # Main engine: generate_text, max_subsequence_embedding
│ │ ├── data.rs # Wordlist loading: load_payload_words, load_cover_words_by_pos
│ │ ├── cache.rs # SequenceCache: precomputed POS sequences for all k values
│ │ └── utils.rs # Helpers: normalize_token, wrap_payload, word_wrap
│ │
│ └── (library modules only — binaries live in glossia-cli / glossia-tools)
│
├── languages/
│ ├── english/
│ │ ├── grammar.yaml # English grammar with Montague types
│ │ ├── cover.yaml # Cover wordlist (BIP39-disjoint)
│ │ ├── payload_bip39.yaml # BIP39 payload wordlist
│ │ ├── cover_ngram.yaml # N-gram cover wordlist
│ │ ├── payload_ngram.yaml # N-gram payload wordlist
│ │ └── payload_lemmas.yaml # WordNet lemma payload
│ ├── latin/
│ │ ├── grammar.yaml # Latin grammar rules
│ │ ├── cover.yaml # Latin cover words
│ │ ├── payload.yaml # Latin payload (65,536 words)
│ │ ├── payload_hp.yaml # Harry Potter themed payload
│ │ ├── dialect.yaml # Dialect overlays
│ │ └── pos_mapping.yaml # POS tag mapping
│ ├── meta/
│ │ ├── grammar.yaml # Dialect calculus meta-grammar
│ │ ├── payload.yaml # Dialect identifiers (language/format names)
│ │ └── cover.yaml # Pipeline function words (verbs, preps, etc.)
│ ├── math/
│ │ └── primes/
│ │ ├── grammar.yaml # Arithmetic grammar
│ │ ├── payload.yaml # Prime numbers
│ │ └── cover.yaml # Composite numbers
│ ├── hp/ # Harry Potter language
│ ├── french/ # French BIP39 (text only)
│ └── german/ # German BIP39 (text only)
│
├── docs/ # mdBook documentation
│ ├── book.toml
│ ├── src/ # Markdown source
│ └── book/ # Generated HTML
│
└── .claude/agents/ # Claude Code agent profiles
├── linguist.md
├── language-designer.md
├── cryptographer.md
├── rust-engineer.md
├── mathematician.md
├── physicist.md
└── image-artist.md
Core Modules
Generator (src/generator/)
The text generation engine. Takes payload words and produces grammatically correct sentences with those words embedded in-order.
core.rs: Maingenerate_text()function, maximum subsequence matching algorithm, sentence planning and slot fillingtypes.rs: Key types includingPayloadTok(tagged payload word),Lexicon(cover word store with POS indexing and refinement tags),GenerationMode,SentenceLengthModedata.rs: Loads wordlists from embedded YAML, builds POS mappings, tags wordscache.rs:SequenceCacheprecomputes all valid POS sequences for lengths k_min to k_maxutils.rs: Token normalization, payload highlighting, word wrapping
Grammar (src/grammar.rs)
Loads YAML grammar files and expands CFG productions. Supports weighted alternatives, refinement tags (Det[def], Det[indef]), and dialect overlay (base rules + dialect-specific overrides).
Type System (src/semantic_types.rs, src/lambda_terms.rs)
Montague Grammar types and lambda calculus terms. Each POS tag maps to a semantic type (e.g., N -> e -> t), and grammar rules carry lambda terms for type-driven composition.
Codec (src/codec.rs)
Binary encoding/decoding of arbitrary data to wordlist words. Uses bit-packing: each word represents log2(wordlist_size) bits. A padding word at the start indicates trailing zero bits.
Merkle (src/merkle.rs)
Merkle tree construction over payload word sequences. merkleize() wraps N payload words into a 2N-1 word sequence with cover words as internal nodes. parse_merkleized() extracts the original payload. verify_merkleized() provides cryptographic verification.
Build System (build.rs)
The build script:
- Scans
languages/for all YAML files - Validates cover/payload disjointness for each language
- Generates
language_index.rswithinclude_str!()for all files - In debug mode, only embeds English (faster iteration)
- Provides
has_embedded_files()function for runtime language detection
Dependencies
| Crate | Version | Purpose |
|---|---|---|
rand | 0.8 | Random selection of cover words and productions |
nlprule | 0.6 | NLP and POS tagging (for tools) |
clap | 4.4 | CLI argument parsing |
pest / pest_derive | 2.7 | Parser generator (lambda expressions) |
serde / serde_yaml / serde_json | 1.0 / 0.9 / 1.0 | Serialization |
moniker | 0.5 | Lambda calculus variable binding |
bincode | 1.3 | Binary encoding |
rayon | 1.11 | Parallel processing |
reqwest | 0.11 | HTTP (for downloading frequency data) |
regex | 1.10 | Pattern matching |
flate2 | 1.0 | Gzip compression |
csv | 1.3 | CSV parsing |
anyhow | 1.0 | Error handling |
Deployment & CI
Glossia uses GitHub Actions for continuous integration and for deploying the WASM web app to GitHub Pages.
Workflows
| Workflow | File | Trigger | Purpose |
|---|---|---|---|
| CI | .github/workflows/ci.yml | every PR, push to master | cargo build + cargo test |
| Deploy Web App | .github/workflows/deploy-web.yml | push to master (web/src/Cargo/languages paths) + manual | Build WASM, deploy production site |
| PR Preview | .github/workflows/pr-preview.yml | PR opened/updated/reopened/closed | Build WASM, deploy a per-PR preview |
GitHub Pages source
Both the production deploy and PR previews publish to the gh-pages
branch (production at the root, previews under pr-preview/pr-<N>/). Pages
can only serve from a single source, so they must share one branch.
One-time setup: In the repository settings under Settings → Pages, set the Source to Deploy from a branch and choose the
gh-pagesbranch (folder/ (root)). Thegh-pagesbranch is created automatically by the first production deploy after this change.The custom domain (
glossia.io) is preserved via theCNAMEfile inweb/, which the production deploy publishes to the root ofgh-pages.
How previews work
On every push to a (non-fork) pull request, the PR Preview workflow builds
the WASM bundle and deploys web/ into pr-preview/pr-<N>/ on the gh-pages
branch using rossjrw/pr-preview-action.
The action posts a comment on the PR with the live URL, e.g.
https://glossia.io/pr-preview/pr-42/. When the PR is closed or merged, the
preview directory is removed automatically.
Production deploys use
JamesIves/github-pages-deploy-action
with clean-exclude: pr-preview/, so publishing a new production build never
wipes the live previews of open PRs.
The web app loads its WASM via relative paths (./glossia.js, with init()
resolving the .wasm relative to the module URL), so it works correctly when
served from a subpath.
WASM bundle size
The web app ships as a single glossia_bg.wasm module, so keeping it small
matters for first-load time. The bundle is dominated not by code but by the
language data embedded at compile time.
What dominates the bundle
build.rs embeds every languages/**/*.yaml file into release builds via
include_str!, plus a precomputed sorted word-index (.txt) for each payload
wordlist. The English wordlists dwarf everything else:
| Embedded file | Size | Used by web app? |
|---|---|---|
english/payload_lemmas.yaml | ~4.1 MB | No — not referenced by any dialect |
english/payload_ngram.yaml | ~3.5 MB | No |
english/cover_ngram.yaml | ~3.5 MB | No |
english/wordnet_lemmas.yaml | ~2.2 MB | No — WordNet source data |
| All other languages combined | ~1.7 MB | Yes |
Together those four English files are ~13.3 MB of the ~15 MB of embedded YAML —
and each also contributes a multi-megabyte word-index .txt. They back the
high-density lemmas/ngram encodings (2¹⁷-word lists) available in the CLI,
but no grammar dialect references them, so the web app never exposes them.
Levers applied (issue #21)
- Trim embedded languages in wasm builds.
build.rsdetects the wasm target (CARGO_CFG_TARGET_ARCH == "wasm32") and excludes the large unused English profiles (is_excluded_in_wasm) from every generated lookup — embedded YAML, wordlist profiles, payload word index, and word counts — so the runtime view stays self-consistent. Native (CLI) builds are unchanged and keep all wordlists. This is the single largest win: the rawcargo buildwasm lib drops from ~20.7 MB → ~3.8 MB. - Aggressive size optimisation.
wasm-opt -Oz --strip-debug --strip-producersis configured under[package.metadata.wasm-pack.profile.release]inCargo.toml, so wasm-pack always runs it using its own bundled wasm-opt. (Installing a systemwasm-optis intentionally avoided: an older system binary gets picked up by wasm-pack and rejects the bulk-memory ops modern rustc emits.) - Size-tuned release profile.
[profile.release]enableslto,codegen-units = 1, andstrip, letting the linker drop dead code and symbol info from both native and wasm builds.
To re-measure after changes, build the wasm lib and inspect it:
cargo build --release --target wasm32-unknown-unknown \
--no-default-features --features wasm
ls -l target/wasm32-unknown-unknown/release/glossia.wasm
# Section-level breakdown (if tooling is installed):
# wasm-objdump -h glossia_bg.wasm
# twiggy top glossia_bg.wasm
Limitations
- Fork PRs are skipped. A pull request from a fork uses a restricted
GITHUB_TOKENthat cannot push togh-pages, so no preview is built for it. - Previews share the production domain under
/pr-preview/...; they are not isolated environments.
Glossia: A Type-Theoretic Framework for Encoding Binary Data in Human-Friendly Media
Abstract. We present Glossia, a framework that encodes arbitrary binary data into grammatically correct natural language, aesthetically rendered images, structured musical scores, and prime-factorization arithmetic. At its core, Glossia uses Montague Grammar --- a typed lambda calculus over part-of-speech categories --- to guarantee that every encoding is well-formed according to the rules of its target medium. A context-free grammar (CFG) generates structural frames; payload words carrying data are embedded into type-compatible slots; cover words fill the remainder. Decoding is trivial: filter the output against the payload wordlist. We show that this architecture generalizes beyond prose to any medium where a compositional grammar can be defined over a finite vocabulary, and that a meta-language built from the same type algebra provides type-safe pipelining between all supported media.
1. Introduction
The problem of representing binary data in human-readable form is as old as computing itself. Base64 encodes bytes as ASCII characters. BIP39 maps 11-bit chunks to English words for cryptocurrency seed phrases. QR codes render bits as black-and-white modules. Each solution is domain-specific: the encoding format, the vocabulary, and the output structure are fused into a single design.
Glossia takes a different approach. Rather than designing a new encoding for each medium, it provides a universal framework in which the encoding vocabulary (payload words), the structural grammar (CFG productions), and the output aesthetics (rendering) are independently composable parameters. The key insight is that Montague Grammar --- originally developed to give natural language the same formal rigor as mathematical logic --- provides exactly the type system needed to ensure that data-carrying words are placed only in grammatically valid positions.
The result is a system where:
- A 12-word BIP39 seed phrase becomes a paragraph of grammatically correct English prose.
- The same seed phrase becomes a passage of Latin, a Voronoi diagram of perceptually distinct colors, a sequence of MIDI notes in a pentatonic scale, or a Merkle tree of prime numbers.
- Translation between any two representations is lossless, because all representations factor through a common binary intermediate.
- A meta-language, itself governed by the same type algebra, specifies pipelines like "translate from English into Latin" as grammatically valid sentences whose semantic derivation is the pipeline specification.
Glossia is not steganography. Steganography hides messages in noise --- the goal is secrecy, where an observer cannot detect that a message exists at all. Glossia's goal is privacy: anyone with the matching payload wordlist can immediately see that a Glossia-encoded text carries data. The payload words are not hidden; they are grammatically prominent. This is a deliberate design choice. By keeping payload words easily distinguishable from cover words, Glossia maximizes signal-to-noise ratio and decoding reliability, while freeing the surrounding medium --- prose, images, music --- to optimize for aesthetics and readability rather than obfuscation. The result is closer to a linguistic encoding than a covert channel: the privacy comes from the wordlist (a shared secret), not from the invisibility of the message.
This paper develops the theory, describes the architecture, and presents applications across four media: human language, images, music, and mathematics.
2. Theoretical Foundations
2.1 Montague Grammar and Typed Lambda Calculus
Richard Montague's central thesis (1970) was that natural language can be described by the same mathematical tools as formal logic. His grammar assigns every word a semantic type drawn from a simple type theory:
- e --- the type of entities (individuals, things)
- t --- the type of truth values (propositions, sentences)
- A -> B --- the type of functions from A to B
A noun like "cat" has type e -> t (a predicate: given an entity, it returns a truth value --- "is this entity a cat?"). A transitive verb like "sees" has type e -> (e -> t) (a relation: given an object entity and a subject entity, it produces a proposition). A determiner like "the" has type (e -> t) -> (e -> t) (a quantifier: it takes a predicate and returns a modified predicate).
Sentence construction is function application. "The cat sees the dog" is built by:
- Applying "the"
((e -> t) -> (e -> t))to "cat"(e -> t)to get "the cat"(e -> t). - Applying "sees"
(e -> (e -> t))to "the dog"(e -> t)to get "sees the dog"(e -> t). - Applying "sees the dog" to "the cat" to get a truth value
t.
A sequence of words that reduces to type t is a well-formed sentence. A sequence that does not reduce to t is ungrammatical. Type-checking is grammaticality checking.
2.2 Parts of Speech as Semantic Types
Glossia maps each part-of-speech (POS) tag to a Montague type:
| POS | Type | Linguistic Interpretation |
|---|---|---|
| N (Noun) | e -> t | Predicate over entities |
| V (Verb) | e -> (e -> t) | Relation between entities |
| Adj (Adjective) | e -> e | Entity modifier |
| Adv (Adverb) | (e -> t) -> (e -> t) | Predicate modifier |
| Det (Determiner) | (e -> t) -> (e -> t) | Quantifier |
| Prep (Preposition) | e -> (e -> t) | Prepositional relation |
| Conj (Conjunction) | t -> (t -> t) | Proposition connective |
| Cop (Copula) | (e -> t) -> (e -> t) | Subject-predicate linker |
| Modal/Aux/To | (e -> t) -> (e -> t) | Predicate modifier |
| Pron (Pronoun) | e | Entity reference |
| Dot (Period) | t | Sentence terminator |
This type assignment is language-independent. English, Latin, French, and German all use the same type algebra; they differ only in their CFG productions (word order), their vocabularies, and their morphological features (e.g., Latin has no determiners).
2.3 CCG Combinators
Glossia augments Montague Grammar with five combinators from Combinatory Categorial Grammar (CCG):
| Combinator | Definition | Type | Role |
|---|---|---|---|
| B (Composition) | B f g x = f(g(x)) | (b->c) -> (a->b) -> a -> c | Compose modifier with predicate |
| C (Permutation) | C f x y = f(y)(x) | (a->b->c) -> b -> a -> c | Handle word-order variation |
| T (Type raising) | T x f = f(x) | a -> (a->b) -> b | Lift entity to accept any predicate |
| S (Distribution) | S f g x = f(x)(g(x)) | (a->b->c) -> (a->b) -> a -> c | Share argument between functions |
| I (Identity) | I x = x | a -> a | Passthrough |
These combinators enable flexible word order (the C combinator handles SOV vs. SVO), composition of modifiers (the B combinator composes adjectives with nouns), and argument sharing (the S combinator applies two functions to the same input).
2.4 Refinement Types
To distinguish between dialects and language-specific features, Glossia extends the base type system with refinement types:
Refined("latin/body", e) -- Latin prose as a data representation
Refined("hex", e) -- Hexadecimal string
Refined("bits", e) -- Raw bitstream
Refined("pentatonic/C", e) -- Pentatonic scale rooted at C
Refinement subsumption follows a slash-hierarchical convention: e accepts e[latin], and e[latin] accepts e[latin/body], but e[latin] does not accept e[english]. This gives the type system the precision to distinguish between dialects while allowing generic operations to work across all dialects of a family.
3. Architecture
3.1 Two-Layer Grammar
Glossia uses a two-layer grammar system:
Layer 1: Context-Free Grammar (CFG). Weighted production rules generate sequences of POS tags. For example:
S -> NP VP Dot [weight 0.85]
NP -> Det N [weight 0.50]
NP -> Det Adj N [weight 0.10]
VP -> Modal V NP [weight 0.30]
VP -> Cop Adj [weight 0.15]
Weights determine probabilistic selection between alternative productions. A compact mode selects the shortest sentence that fits the payload; a natural mode samples from the grammar's length distribution for more varied output.
Layer 2: Type-Driven Constraints. Each CFG production has an associated lambda term that specifies how its constituents compose semantically. The type checker verifies that the lambda term is well-typed --- that the POS sequence can reduce to type t. This prevents the CFG from generating nonsensical combinations like Det Det V that would be syntactically derivable but semantically ill-formed.
Grammar rules are defined in YAML:
noun_phrase:
lambda: "λn:(e->t). n"
cfg_productions:
- production: "Det[def] Adj N"
weight: 0.10
lambda: "λn:(e->t). λadj:(e->e). λdet:((e->t)->(e->t)). det(n)"
3.2 Payload and Cover Wordlists
Every Glossia language maintains two disjoint wordlists:
-
Payload wordlist: Words that carry data. Each word maps to a unique index; the index represents a binary value. For a wordlist of size $2^b$, each word encodes $b$ bits. BIP39's 2048-word list gives 11 bits per word. Glossia's Latin vocabulary of $2^{16}$ words gives 16 bits per word.
-
Cover wordlist: Function words that provide grammatical scaffolding but carry no data. Determiners ("the", "a"), copulas ("is", "are"), modals ("may", "can"), and sentence terminators (".") are cover words.
POS slots are partitioned into payload-eligible slots (N, V, Adj, Adv, Prep, Det, Conj) and cover-only slots (Aux, Cop, To, Prefix, Dot, Modal). Payload words are POS-tagged with probability distributions across eligible categories, enabling flexible placement.
Both wordlists are strictly append-only. Since decoding maps words to their positional indices, reordering or removing words would corrupt previously encoded messages. Appending new words increases future encoding density without breaking backward compatibility.
3.3 Encoding Process
Given a payload (a sequence of BIP39 words, an ASCII string, a hex key, or raw bytes):
-
Bit-pack the input. Convert the input to a bitstream. Map consecutive $b$-bit chunks to payload word indices. The payload is now a sequence of words.
-
POS-tag the payload. Each payload word has a probability distribution over POS categories. The generator knows which slots each word can fill.
-
Generate a POS frame. The CFG produces a sequence of POS slots (e.g.,
Det N V Det Adj N Dot). The type checker verifies the frame is well-formed. -
Embed payload words. A maximum subsequence matching algorithm places payload words into compatible POS slots, maintaining their original order. Cover words fill the remaining slots.
-
Repeat. If payload words remain after one sentence, generate another sentence and continue embedding.
3.4 Decoding
Decoding is deliberately trivial: filter the output text against the payload wordlist. Every word that appears in the payload wordlist is a payload word; everything else is cover. The extracted payload words, in order, are the encoded message.
This asymmetry --- complex encoding, trivial decoding --- is a design goal. The grammar, the cover words, and the sentence structure are an "aesthetic wrapper" that is discarded entirely during decoding. The wrapper serves human readability; the payload serves machine fidelity.
3.5 Bijectivity and Translation
Because all information resides in the payload word sequence, and payload words biject with bit indices, every Glossia encoding is a bijection between bitstreams and word sequences. This yields several properties:
- Lossless round-trip: Encode then decode recovers the original bitstream exactly.
- Lossless translation: Decode from language A to bits, encode from bits to language B. No information is lost.
- Commutativity: Translating A -> B -> C gives the same result as A -> C, because both factor through the same bitstream.
The bitstream is the zero object in the category of Glossia dialects: every dialect has exactly one encoding morphism to and from the bitstream, and every translation factors through it.
3.6 Merkle Mode
Glossia supports a Merkle tree mode that wraps $N$ payload words in a cryptographic proof structure. The $N$ payload words become leaves of a binary Merkle tree; $N-1$ cover words are assigned as internal nodes (most frequent cover word becomes the root). A pre-order traversal produces the output sequence of $2N-1$ words.
This provides:
- Tamper detection: The Merkle structure proves the payload words are authentic and in the correct order.
- Natural appearance: Common words like "the" appear as the root and internal nodes, maintaining the illusion of natural prose.
- Structural Merkle proofs: The proof structure is carried by the grammatical scaffolding.
4. Application: Human Language Encoding
4.1 English
English is Glossia's default and most developed language. The grammar supports multiple dialects:
- Body: Full sentences with natural variation (
NP VP Dot,NP Adv V NP Dot). Optimized for email body text and prose. - Subject: Shorter phrases (
NP VP,Adj N VP PP). Optimized for email subject lines. - Prose: Flowing sentences with PP chains, conjunctions, and complex NPs for paragraph-length output.
Example encoding of a 12-word BIP39 seed phrase:
Input: snake robot mixed ship program night ten shield bamboo own way yellow
Output: Snake set robot to the mixed ship. Each program is night to each
ten shield. Bamboo own way to the yellow.
The embedded payload words appear in grammatically natural positions. The cover words ("set", "to", "the", "each", "is") provide syntactic glue. Decoding extracts exactly the 12 original words.
4.2 Latin
Latin demonstrates Glossia's cross-linguistic capability. Key differences from English:
- No determiners: Latin has no articles. The grammar omits Det entirely, yielding shorter, denser encodings.
- Flexible word order: Case endings handle grammatical relations, so the CFG can produce SOV, SVO, and other orders.
- Larger vocabulary: With $2^{16} = 65{,}536$ payload words, each Latin word encodes 16 bits --- compared to BIP39's 11 bits. A 132-bit seed phrase requires only 9 Latin words instead of 12 English words.
The same Montague types apply: Latin nouns are still e -> t, verbs are still e -> (e -> t). Only the CFG productions change to reflect Latin syntax.
4.3 Information Density
| Language | Payload Size | Bits/Word | Words for 128-bit Key |
|---|---|---|---|
| English (BIP39) | 2,048 | 11 | 12 |
| Latin | 65,536 | 16 | 8 |
| English (expanded) | 4,096 | 12 | 11 |
Larger vocabularies increase bits per word, yielding more compact encodings. Because wordlists are append-only, expanding a vocabulary increases density for future encodings without breaking backward compatibility.
5. Application: Image Encoding
5.1 Color-Space Encoding
The Glossia image codec encodes binary data into images using a geometric construction in CIELAB perceptual color space. The core idea:
A color palette defines a 1D curve $\gamma$ through CIELAB. At each palette point, a 2D constellation grid in the normal plane encodes additional bits. Every pixel color decomposes into:
- Tangential position on $\gamma$ --- identifies the payload word (which palette color).
- Normal-plane displacement --- encodes the sequence position (which occurrence of that word).
The rendering (Voronoi diagram, pixel grid, mosaic, brush strokes) is purely aesthetic. All information lives in the multiset of colors, not in spatial arrangement.
5.2 Geometric Construction
Palette Curve. A cubic spline through $K$ control points in CIELAB, reparameterized by arc length. $N$ palette colors are equally spaced along $\gamma$:
$$c_i = \gamma!\left(\frac{i \cdot L}{N-1}\right), \quad i = 0, \ldots, N-1$$
Bishop Frame. An orthonormal frame ${T(s), U_1(s), U_2(s)}$ propagated along $\gamma$ via double-reflection (Wang et al., 2008). The Bishop frame is smooth through inflection points, unlike the Frenet frame.
Constellation Grid. At each palette point $c_i$, an $M_i \times M_i$ grid in the normal plane $\text{span}{U_1, U_2}$:
$$\text{point}(a, b) = c_i + \alpha_a \cdot U_1(s_i) + \alpha_b \cdot U_2(s_i)$$
The grid size $M_i$ is determined by the local tube radius --- the largest inscribed disk in the normal plane that stays within the sRGB gamut.
Bits per cell:
$$\text{bits/cell} = \log_2 N + 2 \log_2 M_{\min}$$
With $N = 16$ palette colors and grid spacing $\varepsilon = 2.3$ CIELAB (the just-noticeable difference), each visual element carries 12 bits.
5.3 Decoding
Three decoders are implemented, forming a Pareto frontier:
| Decoder | Complexity | Clean Accuracy | Needs Layout? |
|---|---|---|---|
| Geometric | $O(nS)$ | 100% | Yes |
| Rips filtration | $O(n^2 \log n)$ | 100% | No |
| Spectral | $O(n^3)$ | 92.5% | No |
The geometric decoder projects each pixel onto $\gamma$, snaps to the nearest palette color, decomposes the residual via the Bishop frame, and snaps to the constellation grid.
The Rips filtration decoder uses persistent homology: project all colors onto $\gamma$, build a Vietoris-Rips filtration, and detect the persistence gap that separates within-word merges from between-word merges. The persistence gap equals the palette spacing --- a geometric invariant.
The spectral decoder constructs a Gaussian similarity graph on 1D projections, computes the normalized graph Laplacian, and uses the eigengap to determine the number of distinct words.
5.4 Noise Tolerance and Comparison with QR Codes
Spatial averaging over $P$ pixels per visual element reduces effective noise by $\sqrt{P}$:
$$\sigma_{\text{eff}} = \frac{\sigma_{\text{raw}}}{\sqrt{P}}$$
For a 32-byte Nostr public key at 400x400 resolution:
| Configuration | Elements | Bits/Cell | $\sigma_{95,\text{eff}}$ |
|---|---|---|---|
| QR-L V2 (25x25) | 625 modules | 1 | 18 |
| Voronoi, $\varepsilon$=2.3, 50% ECC | 32 cells | 12 | 110 |
Glossia's image codec achieves 6x greater noise tolerance than QR codes while using 20x fewer visual elements, yielding aesthetically rich images rather than black-and-white grids.
5.5 Screen-to-Camera Channel
For the real-world scenario of photographing an encoded image displayed on a screen, the codec includes calibration cells (one per palette color) that enable color-space-only calibration. A 4-parameter affine camera model captures auto white balance, exposure, and saturation:
$$\begin{pmatrix} L' \ a' \ b' \end{pmatrix} = \begin{pmatrix} 1 & 0 & 0 \ 0 & s & 0 \ 0 & 0 & s \end{pmatrix} \begin{pmatrix} L \ a \ b \end{pmatrix} + \begin{pmatrix} \Delta L \ \Delta a \ \Delta b \end{pmatrix}$$
Grid search over the 4-parameter space ($\sim$53,000 evaluations) identifies both the palette and the camera correction simultaneously. No fiducials, markers, or known pixel positions are required.
6. Application: Music Encoding
6.1 MIDI Notes as Payload Words
Glossia's music language encodes binary data as sequences of MIDI note names. The full chromatic set of 128 notes (C-1 through G9) provides 7 bits per note. The type algebra reinterprets POS categories for music:
| POS | Musical Interpretation |
|---|---|
| N | Payload note (carries data) |
| Adj | Duration (whole, half, quarter, eighth, sixteenth) |
| Adv | Dynamics (pp, p, mp, mf, f, ff) |
| Dot | Barline or double barline |
| Cop | Rest |
| Modal | Articulation (legato, staccato) |
| Aux | Header tokens (tempo, time signature) |
The CFG generates musical structure:
sentence -> BARS Dot[end]
BAR -> BEAT BEAT BEAT BEAT Dot (4/4 time)
BEAT -> N Adj (note + duration)
BEAT -> Cop Adj (rest + duration)
BEAT -> Adv N Adj (dynamic + note + duration)
6.2 Scale-Derived Dialects
Musical scales are defined by interval patterns. A pentatonic scale [2, 2, 3, 2, 3] rooted at C produces pitch classes {C, D, E, G, A}. Applied across all octaves, this yields 45 notes from the 128-note chromatic set.
Scale dialects are specified declaratively in grammar YAML:
pentatonic:
parent: raw
scale:
intervals: [2, 2, 3, 2, 3]
root: C
The system derives the filtered payload wordlist at build time --- no static file needed. Available scales include major pentatonic, minor pentatonic, and blues. Adding a new scale requires only a YAML stanza.
Refinement types ensure type safety: a pentatonic dialect's N slots carry refinement pentatonic/C, preventing accidental mixing with chromatic or blues notes.
6.3 Dialects
- Raw: Notes only, no barlines or headers. Maximum payload density.
- Scored: Header with tempo and time signature, structured bars with barlines, variable bar lengths (4-beat, 3-beat, 2-beat) for musical expressiveness.
The scored dialect's variable bar lengths represent expressive timing, not literal time-signature changes --- a deliberate aesthetic choice that makes the output sound more musical while maintaining the same payload capacity.
7. Application: Mathematics (Prime Encoding)
7.1 Primes as Payload Words
The math/primes language uses prime numbers as payload words. The Merkle tree construction maps a list of leaf primes into a cryptographically structured sequence:
- Input: A list of $n$ leaf primes $[p_1, p_2, \ldots, p_n]$.
- Generate Merkle primes: $n-1$ primes strictly greater than $\max(p_i)$.
- Build tree bottom-up: Pair leaves into internal nodes.
- Assign primes top-down: Root gets the largest Merkle prime, descending by BFS level.
- Pre-order traversal: Output $[root, left_subtree, right_subtree]$.
The grammar maps Merkle tree structure to POS sequences:
| Merkle Concept | POS | Type |
|---|---|---|
| Root | N | e -> t |
| Internal node | V | e -> (e -> t) |
| Leaf | Det | e -> t |
Example productions:
N Det Det -- 2-leaf tree
N V Det Det Det -- 3-leaf tree
N V Det Det V Det Det -- 4-leaf balanced tree
7.2 Prime Ordering Constraint
Cover words in the prime language are composite (non-prime) integers, constrained by:
$$p_{\text{left}} < w_{\text{cover}} < p_{\text{right}}$$
where $p_{\text{left}}$ and $p_{\text{right}}$ are adjacent payload primes. This preserves the ordering invariant needed for deterministic Merkle tree reconstruction. For primes $[3, 5]$, the only valid cover word is 4.
7.3 Properties
- Disjoint sets: All Merkle primes are greater than all leaf primes, enabling deterministic identification.
- Order preservation: Input order is maintained at the leaf level.
- Self-verifying: The Merkle structure provides a built-in integrity proof.
The lambda expression for the complete merkleization process:
merkleize = λleaves: List[Prime].
let max_leaf = fold(max, leaves) in
let n = length(leaves) in
let merkle_primes = generate_primes_after(max_leaf, n-1) in
let tree = build_tree(map(leaf, leaves), merkle_primes) in
let tree_assigned = assign_bfs(tree, reverse(merkle_primes)) in
preorder(tree_assigned)
This maps cleanly to the type system: merkleize : List[Prime] -> List[Prime], transforming $n$ leaf primes into $2n-1$ pre-order traversal primes.
8. The Meta-Language and Pipeline Algebra
8.1 Dialects as Types
Glossia's most powerful abstraction is its meta-language: a grammar built from the same type algebra as the object-level grammars, but with types reinterpreted at the dialect level:
| Object Level | Dialect Level |
|---|---|
e = an entity | e = a data representation (hex, Latin text, bits) |
t = a valid sentence | t = a valid encoded output |
N : e -> t = noun (predicate) | N = encoder (data -> output) |
V : e -> (e -> t) = verb (relation) | V = transcoder (source -> encoder) |
Adj : e -> e = adjective (modifier) | Adj = format transform (decode) |
Det : (e -> t) -> (e -> t) = determiner | Det = DataMode (hex, base64, ascii7) |
Prep : e -> (e -> t) = preposition | Prep = direction ("from", "into") |
8.2 Pipeline Specifications as Sentences
A pipeline specification is a sentence in the meta-grammar:
"translate from english into latin"
^^^^ ^^^^
source target
The meta-grammar uses the same CFG format as object grammars:
sentence:
cfg_productions:
- production: "NP VP Dot" # "encode <data> as <target>."
- production: "VP PP PP Dot" # "transcode from <source> into <target>."
The semantic derivation of this meta-sentence is the pipeline specification. The same type-driven grammar that validates "the cat sees the dog" also validates "the hex decoder transcodes into Latin prose."
8.3 CCG Combinators as Pipeline Algebra
The five CCG combinators have direct pipeline interpretations:
B (Composition) = Transcode. Given decode_latin : Latin -> Bits and encode_english : Bits -> t, their B-composition is:
B encode_english decode_latin : Latin -> t
"Decode Latin to bits, then encode as English." This is the same B N Adj composition used for "the big house" --- compose a modifier with a predicate.
C (Permutation) = Reverse direction. C transcode flips "from Latin into English" to "into English from Latin."
T (Type raising) = Lift data to pipeline. Given data nostr_sig : e, type raising produces T nostr_sig : (e -> t) -> t --- "here is a Nostr signature; give me any encoder."
S (Distribution) = Fork. S transcode_english transcode_latin data encodes the same data in two formats simultaneously.
8.4 The Bitstream as Zero Object
Every dialect $D$ that carries payload is isomorphic to the bitstream:
$$\text{encode}_D : \text{Bits} \to D, \quad \text{decode}_D : D \to \text{Bits}$$
$$\text{decode}_D \circ \text{encode}D = \text{id}{\text{Bits}}, \quad \text{encode}_D \circ \text{decode}_D = \text{id}_D$$
Every morphism between dialects factors through bits:
$$\text{hom}(D_1, D_2) = {\text{encode}{D_2} \circ \text{decode}{D_1}}$$
In category-theoretic terms, Bits is a zero object in the category of dialects. This is why translation is always lossless and commutative: $A \to B \to C = A \to C$.
8.5 DataMode as Natural Transformation
The four DataModes (Bytes8, Ascii7, Base64, Hex) are natural transformations between encoding functors. A DataMode $m$ transforms any encoder enc : e -> t into m(enc) : e -> t. In the type system, this is exactly the Det type: (e -> t) -> (e -> t).
The naturality condition states that applying a DataMode commutes with transcoding:
$$B ; (m(\text{encode}_B)) ; \text{decode}_A = m(B ; \text{encode}_B ; \text{decode}_A)$$
This holds because DataModes operate on bits, which are invariant under transcoding.
8.6 Curry-Howard Correspondence
The dialect calculus inherits the Curry-Howard correspondence directly:
| Lambda Calculus | Logic | Dialect Calculus |
|---|---|---|
Type A -> B | "A implies B" | "Given format A, I can produce format B" |
Term f : A -> B | Proof of "A implies B" | A concrete encoder/decoder |
Application f(x) | Modus ponens | Running the pipeline on data |
Composition B f g | Transitivity | Transcoding via intermediate |
t | Theorem | Valid encoded output exists |
| Type error | Contradiction | Incompatible pipeline |
Type-checking is proof verification. When the type checker validates:
hex_mode(B encode_latin hex_decode)(nostr_sig) : t
it simultaneously verifies that "given a Nostr hex signature, a hex decoder, and a Latin encoder, a valid Latin encoding exists." If any component has the wrong type, the proof fails before any data flows.
Uninhabited types mean impossible pipelines. If no term of type Refined("hex", e) -> Refined("latin/body", e) exists, then you must factor through Bits. The type system forces this to be explicit.
8.7 Worked Example: Nostr Signature in Latin
"Encode a hex Nostr signature as Latin prose."
Step 1. Assign dialect types:
nostr_sig : Refined("nostr/sig", e) -- the data
hex_decode : Refined("hex", e) -> Refined("bits", e) -- hex -> bytes
encode_latin : Refined("bits", e) -> t -- bytes -> Latin
hex_mode : (e -> t) -> (e -> t) -- DataMode::Hex
Step 2. Build the pipeline:
hex_mode(B encode_latin hex_decode)(nostr_sig) : t
Step 3. Type-check:
hex_mode : (e -> t) -> (e -> t)
B encode_latin hex_decode : e -> t
hex_mode(B encode_latin hex_decode) : e -> t
nostr_sig : e
────────────────────────────────────────────
hex_mode(B encode_latin hex_decode)(nostr_sig) : t ✓
The pipeline is well-formed. The output is valid Latin prose encoding the Nostr signature.
9. Self-Similarity of Object and Meta Levels
A striking property of Glossia's architecture is that the meta-language has the same grammar as the object language. This is not a coincidence: both levels use the same type algebra (e, t, ->, Refined). The grammar is a free algebra over these types. Lifting the interpretation of e from "entity" to "data representation" lifts the entire grammar.
This self-similarity means:
- One type checker serves both levels. The same Rust code that validates English sentences validates pipeline specifications.
- One parser serves both levels. The lambda parser, the CFG engine, and the POS tagger are reused without modification.
- The meta-language is itself a Glossia language. It has its own payload wordlist (dialect names: "latin", "english", "hex", "bits"), its own cover wordlist (pipeline verbs: "encode", "decode", "translate", "from", "into"), and its own grammar productions.
- Meta-meta levels are possible. A grammar that describes how to compose meta-language pipelines would use the same type system. The tower of grammars is limited only by practical utility, not by architectural constraints.
10. Error Correction and Transmission
Glossia is a pure encoding layer: it faithfully represents whatever bits it is given, without assumptions about what those bits mean. This design separates concerns cleanly:
- Checksums, error correction codes, and metadata are encoded as part of the payload bitstream itself.
- Glossia handles encoding/decoding: the language layer converts bits to readable form and back.
- Your protocol handles integrity: checksums and error correction verify the decoded bits.
For the image codec specifically, Reed-Solomon error correction wraps the parametric encoder for fair comparison against QR codes. The RS layer is optional and additive --- it increases the number of visual elements but enables correction of residual decoding errors.
11. Implementation
Glossia is implemented in Rust with the following key components:
| Module | Purpose |
|---|---|
semantic_types.rs | SemanticType enum, refinement subsumption, type application |
lambda_terms.rs | LambdaTerm enum, CCG combinators, beta reduction |
lambda_parser.rs | Pest-based parser for lambda expressions |
type_driven_grammar.rs | LanguageConfig, TypeRule, generation from types |
grammar.rs | CFG parser, weighted production selection |
codec.rs | Bit-packing, DataMode encoding/decoding |
pipeline.rs | Meta-language-driven translation between languages |
merkle.rs | Append-only wordlist trees, Merkle proof construction |
scale.rs | Musical scale derivation from interval patterns |
Language definitions reside in languages/{language}/grammar.yaml with associated wordlists. All language files are embedded at compile time via build.rs, with debug builds embedding only English for fast iteration.
The system compiles to a single binary with no runtime file dependencies. A WebAssembly target enables browser-based encoding and decoding.
12. Related Work
BIP39 (Bitcoin Improvement Proposal 39) established the practice of encoding cryptographic entropy as natural-language word sequences. Glossia generalizes BIP39 by adding grammatical structure and cross-language translation.
Montague Grammar (Montague, 1970) provided the theoretical foundation for treating natural language with the formal rigor of mathematical logic. Glossia applies Montague's type system operationally, as a constraint on data encoding rather than a tool for semantic analysis.
Combinatory Categorial Grammar (Steedman, 2000) extended Montague's approach with combinators for flexible word order and composition. Glossia uses the B, C, S, T, I combinators for both sentence construction and pipeline specification.
Steganography research has explored hiding data in text (linguistic steganography) and images. Glossia differs from steganography in a fundamental way: steganography provides secrecy (an observer cannot detect that a message exists), while Glossia provides privacy (anyone with the payload wordlist can see the data is there). This distinction is analogous to the difference between a locked diary written in plain English (private) and invisible ink on a blank page (secret). In Glossia, payload words are grammatically prominent and trivially extractable given the wordlist --- there is no attempt to make them indistinguishable from cover words. This transparency is a feature: by boosting signal above noise rather than burying it, Glossia achieves reliable decoding and frees the surrounding medium to optimize for human readability and aesthetic quality rather than statistical undetectability.
QR codes (ISO/IEC 18004) encode binary data as 2D black-and-white matrices. Glossia's image codec achieves higher information density per visual element and greater noise tolerance, at the cost of requiring a color-capable display and camera.
13. Conclusion
Glossia demonstrates that a single type-theoretic framework --- Montague Grammar extended with CCG combinators and refinement types --- can govern the encoding of binary data into arbitrarily diverse media. The key contributions are:
-
A universal encoding architecture where vocabulary, grammar, and rendering are independently composable parameters, connected by a shared type system.
-
A proof that grammaticality checking and type-checking are the same operation, enabling the same infrastructure to validate English sentences, Latin prose, musical scores, Merkle trees, and pipeline specifications.
-
A meta-language that uses the identical type algebra to compose encoding pipelines, making translation between media type-safe and algebraically composable.
-
Concrete applications across four media --- human language, images, music, and mathematics --- demonstrating that the framework is not merely theoretical but practically effective.
The bitstream as zero object, the Curry-Howard correspondence between pipeline type-checking and proof verification, and the self-similarity between object and meta levels suggest that Glossia is not just a tool but a calculus of representation --- a formal language for talking about how information can be presented to humans.
References
- Montague, R. (1970). "Universal Grammar." Theoria, 36(3), 373--398.
- Steedman, M. (2000). The Syntactic Process. MIT Press.
- Wadler, P. (2015). "Propositions as Types." Communications of the ACM, 58(12), 75--84.
- Wang, W., Juttler, B., Zheng, D., & Liu, Y. (2008). "Computation of rotation minimizing frames." ACM Transactions on Graphics, 27(1), 1--18.
- ISO/IEC 18004:2015. "Automatic identification and data capture techniques --- QR Code bar code symbology specification."
- Bitcoin Improvement Proposal 39 (BIP39). "Mnemonic code for generating deterministic keys."