Introduction
The Conlang kit is meant to assist in the creation of constructed languages. On a simple level, it can be used as a name, word, or sentence generator, but it also encompasses orthography, grammar, phonetic shifts, and helps manage your lexicon (which can include borrowed words).
What distinguishes this toolkit from others is the focus on phonetics. It is meant to get to get you from zero to hearing a language spoken as quickly as possible. The Voices section covers setting up text-to-speech so you can hear your language. This allows for rapid iteration on adding or removing noises and patterns to get to something that sounds like what you want.
Biases
The author of this library is a native English speaker with a midwestern accent and is generally familiar with European languages. While the International Phonetic Alphabet is intended to capture vocalizations from around the world, the examples used will have a Euro-centric bias. This is especially evident in the lack of good support for non-pulmonic consonants.
Language Fundamentals
Phonemes
NOTE
This page acts as a quick introduction to phonetics. If you are already familiar with phonetics, feel free to skip down to inventory. This page is also the reference for the predefined phonetic categories (e.g.:
Ḍ,Q,I, etc) that are useful for generation based on photological constraints, so you will likely be coming back to this page.
A phoneme is the basic unit of sound in speech.
These are captured in the International Phonetic Alphabet, which provides a mapping of these sounds into symbols.
In examples, IPA is written /laɪk ðɪs/ (“like this”) – monospaced and surrounded by /s.
Reading IPA does not come naturally, partially due to the abundance of funny symbols like ɤ, ɮ, and ɰ.
You might be familiar with e, but what are ɘ and ə supposed to mean?
Searching for these is also a challenge, since ʔ will turn up that ʔ is the symbol for an unaspirated glottal stop,
which is not helpful until you know what that means.
Another hurdle is that symbols sometimes do not align with their written-language meanings; for example, the IPA /fit/
is pronounced like “feet,” not “fit” as the spelling might seem.
The best way to familiarize yourself with what these symbols sound like is to use the IPA Chart and click
the symbols to listen to them.
Note that vowels are pronounced directly, while consonants get a vowel sound to accentuate it (try pronoucing /d/) on
its own).
After getting a baseline, look up some words in the dictionary and show their IPAs.
Copy and paste these into the IPA Reader and you can start deconstructing words to see what happens.
You can also do this with conlang speak ....
IPA
Consonants
Consonants are categorized based on the place and manner of articulation.
The place of articulation refers to where in the vocal tract the sound comes from; a /p/ is bilabial, meaning it
comes from your lips pressing together, whereas a /k/ is velar, coming from the back of the mouth.
The manner of articulation refers to how the sound is made.
This is easiest to distinguish when looking at sounds with the same place; /t/, /n/, and /s/ are all alveolar
(tongue pressed on the top of your front teeth), but the plosive (full stop), nasal (air goes into the nose), and
fricative (restricted hissing) manners make different sounds.
| Place | Bilabial | Labiodental | Dental | Alveolar | Post alveolar | Retroflex | Palatal | Velar | Uvular | Pharyngeal | Glottal | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Manner | M | L | D | Ḍ | J | G | Q | H | |||||||||||||||
| Plosive | P | p | b |
t | d | ʈ | ɖ |
c | ɟ |
k | g |
q | ɢ |
ʔ | |||||||||
| Nasal | N | m |
ɱ |
n | ɳ |
ŋ |
ɴ |
||||||||||||||||
| Trill | T | ʙ |
r | ʀ |
|||||||||||||||||||
| Tap/Flap | ⱱ |
ɾ | ɽ |
||||||||||||||||||||
| Fricative | X | ɸ | β |
f | v |
θ | ð |
s | z |
ʃ | ʒ |
ʂ | ʐ |
ç | ʝ |
x | ɣ |
χ | ʁ |
ħ | ʕ |
h | ɦ |
| Lateral Fricative | ɬ | ɮ |
|||||||||||||||||||||
| Approximant | R | ʋ |
ɹ |
ɻ |
j |
ɰ |
|||||||||||||||||
| Lateral Approximant | l |
ɭ |
ʎ |
ʟ |
|||||||||||||||||||
The boldface capital letters at the top of the columns and left of the rows represent the predefined phonetic categories
available for generation.
For example, N can be used for any of m, ɱ, n, ɳ, ŋ, ɴ.
Some of these categories span multiple places or manners of articulation.
D covers dental, alveolar, and post alveolar; T covers trill, tap, and flap; and H covers pharyngeal and
glottal.
These letters do not come from any official phonetic studies, but serve as convenience when writing
phonological constraints.
Vowels
| Front | Mid | Back | |||
|---|---|---|---|---|---|
| Close | i y | ɨ ʉ | ɯ u | ||
ɪ ʏ | ʊ | ||||
| Close-mid | e ø | ɘ ɵ | ɤ o | ||
ə | |||||
| Open-mid | ɛ œ | ɜ ɞ | ʌ ɔ | ||
æ | ɐ | ||||
| Open | a ɶ | ɑ ɒ |
Non-pulmonic Consonants
| Clicks | Voiced Implosives |
|---|---|
ʘ Bilabial | ɓ Bilabial |
ǀ Dental | ɗ Dental |
ǃ Postalveoalar | ʄ Palatal |
ǂ Palatoalveolar | ɠ Velar |
ǁ Alveolar Lateral | ʛ Uvular |
Other
Inventory
Your phonetic inventory is the set of available sounds which are native to that language.
The simplest representation of this inventory is a string of consonants and vowels. In JSON, this is an object with a couple of keys. The non-pulmonic constants and others are not required, but are shown here to demonstrate their defaults.
{
"consonants": "pbɣʂʐɟkgqʕβt",
"vowels": "ɘɵæiəɯɨɑɤyɛœaʉ",
"non_pulmonics": "",
"others": ""
}
Phonotactics
Phonotactics define which sounds can appear where in a syllable or word. In this toolkit, you express phonotactic rules as pattern strings that describe the structure of generated syllables.
Pattern Codes
Basic
| Code | Meaning |
|---|---|
C | Any consonant from the inventory |
V | Any vowel from the inventory |
Spaces separate syllables within a word.
For example, CVC CV generates a two-syllable word: one CVC syllable followed by one CV syllable.
Place of Articulation
These codes select consonants by where in the vocal tract the sound is produced. See the phonemes consonant table for the full mapping.
| Code | Places |
|---|---|
M | Bilabial |
L | Labiodental |
D | Dental, Alveolar, Post alveolar |
Ḍ | Retroflex |
J | Palatal |
G | Velar |
Q | Uvular |
H | Pharyngeal, Glottal |
Manner of Articulation
These codes select consonants by how the sound is produced.
| Code | Manners |
|---|---|
P | Plosive |
N | Nasal |
T | Trill, Tap, Flap |
X | Fricative, Lateral Fricative |
R | Approximant, Lateral Approximant |
A | Affricate |
Bracket Sets [...]
Brackets specify an exact set of IPA phonemes to choose from, regardless of the inventory.
[ptk]VC: one of /p/, /t/, or /k/, then any vowel, then any consonantC[aiu]C: any consonant, then one of /a/, /i/, or /u/, then any consonant
The contents are parsed as IPA, so compound segments and diacritics work:
[t͡ʃd͡ʒ]VC: one of /t͡ʃ/ or /d͡ʒ/, then vowel, then consonant[tʰpʰkʰ]V: one of aspirated /t/, /p/, or /k/, then vowel
Optional Segments (...)
Parentheses make a single position optional – it has a 50% chance of appearing.
(C)VC: optional onset – generates either VC or CVCCV(C): optional coda – generates either CV or CVC(C)V(C): both optional – generates V, VC, CV, or CVC
Brackets work inside parentheses:
([ptk])VC: optionally one of /p/, /t/, or /k/, then vowel, then consonant
Examples
A simple CV language:
conlang generate-words --consonants ptkmnsl --vowels aeiou --pattern "CV"
A language with CVC syllables and optional onsets:
conlang generate-words --consonants ptkbdgmnŋsʃlr --vowels aeiou \
--pattern "(C)VC"
Two-syllable words with specific initial consonants:
conlang generate-words --consonants ptkbdgmnŋsʃlr --vowels aeiou \
--pattern "[ptkbdg]VC CV"
Nasal-only codas with optional fricative onsets:
conlang generate-words --consonants ptkbdgmnŋsʃfvlr --vowels aeiou \
--pattern "(X)VN"
Named Sets $name
Named sets let you define custom groups of phonemes (including consonant clusters) in the JSON sketch and reference them by name in patterns. This is essential for phonotactic constraints where the set of allowed segments differs by position – for example, onsets allowing clusters like /st/ or /pl/ while codas allow /ks/ or /nd/.
Defining sets in JSON
Add a "sets" field to your sketch.
Each key is a set name (referenced as $<name> in patterns), and each value is an array of IPA strings:
{
"consonants": "pbtdkgmnŋfvszhrl",
"vowels": "aeiou",
"sets": {
"O": ["p", "b", "t", "d", "st", "sp", "tr", "pl"],
"K": ["p", "t", "k", "m", "n", "ŋ", "nd", "ks"],
"longer": ["ŋ", "d", "b"]
},
"word_classes": {
"word": {
"patterns": ["$OV$K", "$OV", "V$K", "V${longer}"]
}
}
}
The
"word_classes"supports different types of words. When you get to grammar, you will see how these are used.
Each entry in the array can be:
- A single segment:
"p","ŋ" - A cluster (multi-segment sequence):
"st","pl","nd" - A weighted entry:
{"value": "st", "weight": 5}– plain strings get weight 1 when any entry has an explicit weight
Using sets in patterns
Reference a named set with $ followed by the set name.
For single-character names, use $O directly.
For multi-character names, use curly braces: ${onset}.
| Pattern | Meaning |
|---|---|
$OV$K | Onset from set O, then vowel, then coda from set K |
${onset}V${coda} | Same, using multi-character set names |
$OV | Onset from set O, then vowel (open syllable) |
($O)V$K | Optional onset from set O, then vowel, then coda |
(${onset})V${coda} | Same with braces |
Onset and coda restrictions
Named sets are the primary way to express positional constraints. For example, English allows /st/ as an onset cluster but not as a coda, while /ks/ is a valid coda but not an onset:
"sets": {
"O": ["n", "t", "s", "st", "sp", "sk", "tɹ", "pɹ", "pl", "bl", "kl"],
"K": ["n", "t", "s", "k", "ŋ", "nd", "nt", "ns", "ks", "lz", "lk"]
}
Weighted sets
If any entry in a set uses the weighted form, all entries participate in weighted random selection. Plain strings receive weight 1:
"O": ["p", "t", {"value": "st", "weight": 5}]
Here, “st” is five times more likely to be selected than “p” or “t”.
Frequencies
This page covers the frequencies of fragments that you find in phonemes and
phonological constraints.
When generating sounds and text, how frequently should certain things be used?
For example, if a word is being generated with consonant inventory "pbɣʂʐɟkgqʕβt" and the rule calls for a consonant,
how often should /p/ be chosen vs /b/ vs … vs /β/ vs /t/?
By default, generation follows the sinusoidal distribution pattern, but this can be customized.
Distributions
Uniform
The most basic frequency distribution is a uniform one.
Sinusoidal
The sinusoidal distribution is based on \( \cos{x} |_{0}^{\pi/2} \). Given \( k \) possible elements, the starting position on the curve for the \( n \)th element is given by \( p(n) \), with ending position at \( p(n+1) \):
\[ p(n) = \frac{n\pi}{2k} \]
The probability of choosing the \( n \)th element is given by the integral from \( p(n) \) to \( p(n+1) \):
\[ \int_{p(n)}^{p(n+1)} \cos x = \left[ \sin x \right]_{p(n)}^{p(n+1)} \]
As an example, the consonant inventory "pbɣʂʐɟkg" would have the distribution:
p | b | ɣ | ʂ | ʐ | ɟ | k | g |
|---|---|---|---|---|---|---|---|
| 19.5% | 18.7% | 17.2% | 15.1% | 12.4% | 9.2% | 5.6% | 1.9% |
It might seem extreme that p appears approximate 10 times more frequently than g, but most languages follow such
extreme patterns.
This is why Scrabble rewards you 10 times the points for playing a “Q” than an “S.”
Customizing Cosine
The distribution \( f \) can be customized over its \( a \) parameter:
\[ f(x; a) = \cos x + a |_{0}^{\pi/2} \]
{
"consonants": {
"values": "pbɣʂʐɟkgqʕβt",
"distribution": {
"curve": "cosine",
"a": 0.2
}
}
}
This can even out the extremes of the later-appearing values by giving them a bit of extra weight. In the table below, you can see the effect setting the value of \( a \) (seen in the header) has on the probability distribution of the inventory.
| a → | 0.0 | 0.1 | 0.2 | 0.5 | 1.0 | 2.0 | 10.0 |
|---|---|---|---|---|---|---|---|
p | 13.1% | 12.4% | 11.9% | 11.0% | 10.2% | 9.5% | 8.6% |
b | 12.8% | 12.2% | 11.8% | 10.9% | 10.1% | 9.4% | 8.6% |
ɣ | 12.4% | 11.8% | 11.4% | 10.6% | 9.9% | 9.3% | 8.6% |
ʂ | 11.7% | 11.3% | 10.9% | 10.2% | 9.7% | 9.2% | 8.5% |
ʐ | 10.9% | 10.5% | 10.3% | 9.8% | 9.3% | 8.9% | 8.5% |
ɟ | 9.8% | 9.6% | 9.5% | 9.2% | 8.9% | 8.7% | 8.4% |
k | 8.6% | 8.6% | 8.6% | 8.5% | 8.4% | 8.4% | 8.4% |
g | 7.3% | 7.4% | 7.5% | 7.7% | 7.9% | 8.1% | 8.3% |
q | 5.8% | 6.1% | 6.4% | 6.9% | 7.3% | 7.7% | 8.2% |
ʕ | 4.2% | 4.8% | 5.2% | 6.0% | 6.7% | 7.3% | 8.1% |
β | 2.6% | 3.3% | 3.9% | 5.1% | 6.1% | 6.9% | 8.0% |
t | 0.9% | 1.9% | 2.6% | 4.1% | 5.4% | 6.5% | 7.9% |
In my experience, setting \( a \) to anything above \( 0.2 \) leads to fairly monotonous languages. There is not a lot of character in the difference between a sound appearing 8.6% of the time versus 7.9%. Natural languages frequently have rarely-used productions in them.
Custom
For tighter control of generation, you can specify each element with a custom weight.
{
"consonants": [
{ "value": "ɴ", "weight": 30 },
{ "value": "f", "weight": 20 },
{ "value": "ʝ", "weight": 10 },
{ "value": "θ", "weight": 5 },
{ "value": "b", "weight": 5 }
]
}
For each element \( x \), the probability is that element’s weight divided by the total of all weights:
\[ f(x) = \frac{weight(x)}{\sum_{i \in C} weight(i)} \]
The JSON specification from above gives the probabilities:
ɴ | f | ʝ | θ | b |
|---|---|---|---|---|
| 42.9% | 28.6% | 14.3% | 7.1% | 7.1% |
Rule Interference
Since the same phoneme is not allowed to be generated twice in a row, the constraint rules can interfere with the
observed frequencies in generated patterns.
This is most likely to occur in VV phonotactic patterns, but can occur in any pattern pulling from a repeating pool.
As an example, consider the vowel inventory:
{
"vowels": [
{ "value": "ɘ", "weight": 50 },
{ "value": "ɑ", "weight": 15 },
{ "value": "i", "weight": 5 }
]
}
The pattern VV will generate /ɘɑ/ 3 times more than it will generate /ɘi/ because the weights without /ɘ/ mean
there is a 75%/25% distribution for the remainder.
What might be more disturbing is that /ɑɘ/ will be generated 10 times more than /ɑi/.
This is not usually a problem, since there are usually enough sounds in your inventory to prevent anyone from noticing.
However, it can be an issue when languages have a limited inventory or cases where your phonological constraints are too
restrictive.
Note that these interact; there aren’t that many nasals, so VDN is more restrictive if your consonant set only has
/n/ and /ɴ/.
Stress
Generated words can receive IPA stress marks (ˈ for primary, ˌ for secondary) according to a configurable strategy. Stress is assigned after syllable generation, so it works with both fresh generation and lexicon-based sampling. When morphology is configured, affixation runs first, so stress is computed over the full derived form. Syllables left unstressed are eligible for vowel reduction.
Top-Level Configuration
Add a "stress" field to your sketch to set the default strategy for all word classes:
{
"stress": {
"default": "trochaic",
"secondary": true
}
}
"default" is the stress strategy applied to every word class that does not override it.
"secondary" enables secondary stress on alternating syllables in words with 3 or more syllables.
It defaults to false when omitted.
Strategies
| Strategy | Primary stress position | Typical languages |
|---|---|---|
trochaic | First syllable | English, German |
iambic | Second syllable (first if mono) | French-ish |
penultimate | Second-to-last syllable | Latin, Spanish |
final | Last syllable | French, Turkish |
none | No stress assigned | Function words |
Monosyllabic words receive primary stress unless the strategy is none.
Per-Class Overrides
Each word class can override the default strategy with its own "stress" field.
This is how you keep function words unstressed while content words carry stress:
{
"stress": { "default": "trochaic", "secondary": true },
"word_classes": {
"det": { "patterns": ["CV"], "stress": "none" },
"noun": { "patterns": ["$OV$K", "$OV$K $OV$K"] },
"verb": { "patterns": ["$OV$K"], "stress": "penultimate" }
}
}
Here, determiners get no stress, nouns inherit the top-level trochaic strategy, and verbs use penultimate.
Secondary Stress
When "secondary": true and a word has 3 or more syllables, secondary stress (ˌ) is placed on alternating
syllables aligned with the primary stress, so the word parses into uniform two-syllable feet and
secondary stress never lands adjacent to the primary.
trochaicbuilds strong-weak feet from the start of the word: ˈta.ka.ˌta.kaiambicbuilds weak-strong feet: ta.ˈta.ta.ˌtapenultimateandfinalalternate outward from the primary: ta.ˌta.ta.ˈta.ta and ta.ˌta.ta.ˈta
Without Stress Configuration
When no "stress" field is present at either level, no stress is assigned – words are generated without stress marks.
Note that omitting "stress" is distinct from setting "stress": "none".
Both produce unstressed words, but "none" is meaningful as a per-class override: it keeps specific word classes
unstressed when a top-level default would otherwise give them stress.
This is common for function words; for example, in English, articles like “a” /ə/ and “the” /ðə/ and prepositions
like “to” /tu/ and “of” /ʌv/ carry no stress.
Vowel Reduction
Vowel reduction collapses the vowels of unstressed syllables to a weaker, more central vowel (e.g.:
schwa ə).
This is characteristic of stress-timed languages: English pronounces banana as /bəˈnænə/ rather
than /baˈnana/, Russian merges unstressed /o/ and /a/ (молоко /məlɐˈko/), European Portuguese
reduces unstressed vowels so far they often nearly disappear, and Central Catalan collapses
unstressed /a/, /e/, and /ɛ/ to [ə].
Syllable-timed languages like Spanish, standard Italian, and Japanese instead keep full vowel
quality everywhere; leave reduction off for languages with that rhythm.
Natural languages vary in what unstressed vowels reduce to (Russian [ɐ], European Portuguese [ɨ],
Catalan [u] for back vowels) and in which vowels reduce at all.
The target and targets fields configure both aspects.
Configuration
Add a reduction section to your config JSON:
{
"reduction": { "probability": 0.7, "target": "ə" }
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
probability | number | Yes | Probability that an unstressed syllable’s vowel reduces, between 0.0 and 1.0. |
target | string | One of target/targets | Single vowel that every reduced vowel collapses to. |
targets | object | One of target/targets | Map from source vowels to the vowel they reduce to. Only mapped vowels reduce. |
Exactly one of target and targets must be present.
If the reduction section is absent, no reduction occurs.
Targets
target is the simple form: every vowel in a reduced syllable collapses to that one vowel, as in
the English-style example above.
targets gives per-vowel control.
Each key is a string of one or more IPA vowels; the value is the single vowel they reduce to.
Only the listed vowels reduce — everything else keeps its quality even in unstressed syllables.
A vowel may appear in at most one key.
A Central Catalan–style system, where /a e ɛ/ merge to [ə] and /o ɔ/ raise to [u] while the high vowels /i u/ never reduce:
{
"reduction": {
"probability": 1.0,
"targets": { "aeɛ": "ə", "oɔ": "u" }
}
}
A Russian-style akanye, merging unstressed /o/ and /a/ into [ɐ]:
{
"reduction": {
"probability": 0.9,
"targets": { "oa": "ɐ" }
}
}
Interaction with Stress
Reduction targets whatever syllables are left unstressed by stress assignment, so
which syllables are eligible is determined by the stress configuration:
- Syllables carrying primary stress never reduce.
This includes monosyllabic words, which always receive primary stress (unless the strategy is
none). - A class with
"stress": "none"has every syllable eligible, so the word can reduce entirely. - With
"secondary": true, alternating syllables carry secondary stress and are also protected, halving the eligible syllables in longer words.
Because of this dependency, a sketch with a reduction section but no stress configuration at all
(no top-level stress and no per-class "stress" fields) is rejected as a validation error.
Without stress, every vowel is a candidate for reduction, which leads to an undirected mush of
whatever your target is.
If you still want this, you can explicitly write "stress": "none" at the top-level or per-class.
Behavior
- Reduction runs after stress assignment, so it depends on the final stress marking of each word.
- Only fully unstressed syllables are eligible.
Syllables with primary or secondary stress never reduce.
Secondary stress protects vowel quality, as it does in English (compare the final syllables of
ˈal.pha.ˌbetandˈtrum.pet). - Word classes with
"stress": "none"have every syllable unstressed, so function-word classes (determiners, pronouns, prepositions) reduce throughout — giving the “the → ðə, of → əv” effect. - The whole vocalic nucleus reduces: diacritics on the vowel are dropped, and a diphthong collapses
to a single vowel.
With
targets, a diphthong is keyed by its first component (so with"aeɛ": "ə", /ai/ collapses to [ə] but /ui/ is untouched). Consonants in the onset and coda are untouched. - Reduction applies to a word’s lexicon entry when the vocabulary is pre-generated, so a reduced word form is stable across a session.
- A reduction target may appear in output even if it is not in the configured vowel inventory; targets are outcomes of reduction, not inventory phonemes.
Grammar
Word classes are the core way to define patterns in a language sketch. Each class groups a set of phonotactic patterns and an optional lexicon under a name. Without further structure, sentence generation picks random words from all classes and lines them up. Adding sentence templates gives sentences recognizable structure: “the big cat sat on a mat” works because determiners, nouns, verbs, adjectives, and prepositions each fill a distinct role.
Word Classes
A word class groups a set of phonotactic patterns and an optional lexicon under a name. The names are up to you, but common abbreviations from linguistics include:
| Name | Short for | Examples (English) |
|---|---|---|
det | determiner | the, a, this, some |
noun | noun | cat, idea, river |
verb | verb | run, think, become |
adj | adjective | big, red, careful |
adv | adverb | quickly, very, often |
prep | preposition | in, on, with, from |
pro | pronoun | I, she, they, it |
conj | conjunction | and, but, or |
These names have no special meaning to the tool – they are simply keys in a map.
You could call them a, b, c if you wanted, but descriptive names make your grammar templates easier to read.
If your sketch has only one kind of word (no grammar), a single class named "word" works fine.
Define them in the "word_classes" field of your JSON sketch:
{
"stress": { "default": "trochaic", "secondary": true },
"word_classes": {
"det": {
"patterns": [
{"value": "$OV", "weight": 10},
{"value": "V", "weight": 5}
],
"lexicon": { "generate": { "size": 8 } },
"stress": "none"
},
"noun": {
"patterns": [
{"value": "$OV$K", "weight": 30},
{"value": "$OV$K $OV", "weight": 6},
{"value": "$OV $OV$K", "weight": 4},
{"value": "$OV$K $OV$K", "weight": 3}
],
"lexicon": { "generate": { "size": 120 } }
},
"verb": {
"patterns": [
{"value": "$OV$K", "weight": 25},
{"value": "$OV", "weight": 15},
{"value": "$OV$K $OV", "weight": 5}
],
"lexicon": { "generate": { "size": 80 } }
}
}
}
Each class has its own "patterns" array, which uses the same pattern language described in
phonotactics – C, V, named sets, brackets, optional segments, and weights all work the same way.
Patterns can also be plain strings without weights:
The "lexicon" field is optional.
When present, it pre-generates a fixed vocabulary for that class and samples from it with a
Zipfian frequency distribution, the same way the top-level lexicon works.
When absent, each word is generated fresh from the patterns.
Designing Word Classes
The key insight is that natural languages have two broad categories of words:
Function words (determiners, prepositions, pronouns, conjunctions) are short, few in number, and extremely frequent.
Give them simple patterns like CV or V, small lexicons (5–15 words), and "stress": "none".
Content words (nouns, verbs, adjectives, adverbs) are longer, more varied, and individually less frequent. Give them patterns with codas and multiple syllables, larger lexicons (40–200 words), and let them inherit the default stress strategy.
This asymmetry – combined with stress on content words but not function words – creates the rhythmic alternation that makes sentences feel like language rather than random sound.
Stress Assignment
The top-level "stress" field and per-class "stress" overrides (like "stress": "none" in the example above)
are documented on the Stress page.
Sentence Templates
Templates define the structure of generated sentences. Each template is a space-separated sequence of word class names:
{
"grammar": [
{"value": "det noun verb det noun", "weight": 20},
{"value": "det noun verb", "weight": 15},
{"value": "pro verb det noun", "weight": 12},
{"value": "det adj noun verb det noun", "weight": 8},
{"value": "det noun verb adj", "weight": 5}
]
}
When generating a sentence, the system picks a template (using the same weighted random selection as everywhere else), then generates one word per slot from the corresponding class. The template determines sentence length – there is no separate word-count range.
Every class name used in a template must be defined in "word_classes".
If a template references an undefined class, the sketch will fail to load with an error.
For the lazy, templates can be plain strings and the generator will pick them with equal weight:
{
"grammar": ["det noun verb", "noun verb det noun"]
}
Requirements
"word_classes" is required in every sketch – it is the only way to define patterns.
"grammar" is optional.
When grammar is present, generate-sentences picks a template and fills each slot from the matching word class.
When grammar is absent, generate-sentences draws each word from a randomly chosen class, with a word count from
the "sentence" configuration (or the default 3–8).
The generate-words command always generates individual words from word classes.
Use --class <name> to restrict output to a single class, or omit it to draw from all classes.
Lexicons
Each word class can have its own lexicon, independent of the others. When a class has a lexicon, generation samples from the pre-built vocabulary rather than creating a new word each time. This means the same “words” recur naturally, with common words appearing more often than rare ones (Zipfian distribution).
Within a single sentence, the same word from the same class will not appear consecutively. Different classes track their repeats independently.
Example
Putting it all together, here is a minimal sketch with word classes:
{
"consonants": "pbtdkgmnŋsʃlr",
"vowels": "aeiou",
"stress": { "default": "trochaic", "secondary": true },
"sets": {
"O": ["p", "t", "k", "b", "d", "g", "st", "pl"],
"K": ["p", "t", "k", "m", "n", "ŋ", "nd", "ks"]
},
"word_classes": {
"det": {
"patterns": ["CV", "V"],
"lexicon": { "generate": { "size": 6 } },
"stress": "none"
},
"noun": {
"patterns": ["$OV$K", "$OV $OV$K"],
"lexicon": { "generate": { "size": 50 } }
},
"verb": {
"patterns": ["$OV$K", "$OV"],
"lexicon": { "generate": { "size": 30 } }
}
},
"grammar": [
{"value": "det noun verb det noun", "weight": 20},
{"value": "det noun verb", "weight": 15},
{"value": "noun verb det noun", "weight": 10}
]
}
conlang generate-sentences --config sketch.json
conlang generate-words --config sketch.json --class noun
The output will have short unstressed determiners (ta, o, ke) alternating with stressed nouns and verbs
(ˈplokŋ, ˈsta.dend), giving each sentence a recognizable rhythmic shape.
Morphology
Morphology adds internal structure to generated words through affixation. Prefixes and suffixes are defined as patterns that probabilistically attach to words of certain classes. This makes words feel derived rather than invented from whole cloth.
Configuration
Add a morphology section to your config JSON:
{
"morphology": {
"decay": 0.5,
"suffixes": [
{
"pattern": "V$K",
"applies_to": ["verb"],
"probability": 0.3,
"label": "past tense"
},
{
"pattern": "C",
"applies_to": ["noun"],
"probability": 0.35,
"label": "plural"
}
],
"prefixes": [
{
"pattern": "CV",
"applies_to": ["verb", "adj"],
"probability": 0.1,
"label": "negation"
}
]
}
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
decay | number | No | Probability multiplier for each subsequent affix of the same type (prefix or suffix). Default 0.5. Must be between 0.0 and 1.0. |
suffixes | array | No | List of suffix definitions. |
prefixes | array | No | List of prefix definitions. |
Affix Fields
| Field | Type | Required | Description |
|---|---|---|---|
pattern | string | Yes | A phonotactic pattern using the same pattern syntax as word classes. Space-separated syllables are supported for multi-syllable affixes. |
applies_to | array | No | Word classes this affix can attach to. If absent, applies to all classes. |
probability | number | Yes | Probability of attachment, between 0.0 and 1.0. |
label | string | No | Documentary label (e.g. “plural”, “past tense”). Has no effect on generation. |
Behavior
Application Order
Morphology runs after word generation but before stress assignment. This means stress is computed over the full derived form including affixes, matching how natural languages work (e.g., English “PHOto” vs “phoTOgraphy”).
Decay
When multiple affixes of the same type (prefix or suffix) are defined, each subsequent attachment multiplies its probability by the decay factor.
For example, with decay: 0.5 and two suffixes both at probability: 0.3:
- First suffix: effective probability = 0.3
- If first attached, second suffix: effective probability = 0.3 * 0.5 = 0.15
- If both attached, a hypothetical third: effective probability = 0.3 * 0.25 = 0.075
This prevents runaway stacking while still allowing occasional multi-affix words.
Lexicon Integration
When a word class uses a pre-generated lexicon, affixes are baked into the lexicon entries at generation time. This means each lexicon word has its affixes fixed, maintaining consistency when the same word is reused.
Class Filtering
If applies_to is specified, the affix only applies to words of those classes.
If applies_to is absent, the affix applies to all word classes.
When no word classes are in use, all affixes apply to all words.
Voices
The whole point of this toolkit is to get from zero to hearing a language spoken. The language fundamentals pages cover how to define sounds, syllable shapes, and grammar – voices are the bridge from IPA on screen to audio in your ears.
NOTE
If you already have a voice driver compiled and configured, skip to CLI Usage for the generation flags or Configuration for the JSON schema. For driver-specific setup, see eSpeak-ng or AWS Polly.
How It Works
When you generate words or sentences, the toolkit produces IPA strings – sequences of phonetic symbols like /ˈpa.ta/ or /ˈka.ba.da/. A voice driver takes those IPA strings and converts them into audible speech through text-to-speech synthesis.
Two drivers are available:
- eSpeak-ng – open-source, runs locally, no network required
- AWS Polly – cloud-based neural TTS with natural-sounding output
Drivers are compile-time feature flags, so you choose which to include when building. You then define named voices in your configuration file, each pointing at a driver with specific settings. The CLI references voices by name.
Getting Started
The fastest path to hearing your language is eSpeak-ng – it runs locally with no cloud account.
Build with the eSpeak feature flag:
cargo build --features voice-espeak
The quickest way to create a conlang.json is to let the CLI scaffold one for you:
conlang config voice init
This writes a starter conlang.json with one example voice per driver you compiled in (eSpeak and/or Polly), which you can then edit.
It refuses to overwrite an existing file unless you pass --force. Use --output <path> to write elsewhere, or --global/-g to write the user-level config at ~/.conlang/config.json.
Or create the file by hand in your project directory with a single voice:
{
"voices": {
"definitions": {
"my-voice": {
"driver": "espeak",
"voice": "en",
"rate": 150
}
},
"default": "my-voice"
}
}
Generate some words and hear them:
conlang generate-words --consonants ptkmnsl --vowels aeiou --pattern "CVC" --speak
The --speak flag picks from all configured voices (here, just the one).
To select a specific voice by name, use --speak-with my-voice instead.
Configuration
Config File Location
The toolkit searches for voice configuration in this order:
| Priority | Location | Notes |
|---|---|---|
| 1 | --voice-config <path> | Explicit CLI override |
| 2 | ./conlang.json | Project-local config |
| 3 | ~/.conlang/config.json | User-level config |
The "voices" section can coexist with language sketch data in the same file – the tool ignores sections it
does not need for a given command.
JSON Structure
A complete example with multiple voices across both drivers:
{
"voices": {
"definitions": {
"polly-joanna": {
"driver": "polly",
"voice_id": "Joanna",
"engine": "neural"
},
"polly-matthew": {
"driver": "polly",
"voice_id": "Matthew",
"engine": "standard"
},
"espeak-de5": {
"driver": "espeak",
"voice": "mb-de5",
"rate": 120,
"pitch": 50,
"volume": 100
},
"espeak-default": {
"driver": "espeak",
"voice": "en",
"rate": 150
}
},
"default": "espeak-de5"
}
}
"definitions" is a map of voice names to driver configurations.
The names are yours to choose – they are the values you pass to --speak-with on the command line.
"default" sets which voice to use when --speak-with is passed the value "default" or when no name is
otherwise specified.
It must match one of the keys in "definitions".
Each entry requires a "driver" field ("espeak" or "polly") and accepts driver-specific fields described
on the eSpeak-ng and AWS Polly pages.
Choosing a Driver
| eSpeak-ng | AWS Polly | |
|---|---|---|
| Cost | Free | Pay-per-character |
| Network | Offline | Requires internet |
| Voice quality | Robotic / formant | Neural / natural |
| Latency | Immediate | Network round-trip |
| Setup | Install system package | Configure AWS credentials |
| IPA fidelity | Approximate (via conversion) | Direct (via SSML) |
eSpeak-ng is the best choice for rapid iteration – no network, no cost, instant feedback. AWS Polly shines when you want to hear something closer to natural speech, such as for demos or recordings.
CLI Usage
Speaking Generated Output
The --speak-with and --speak flags work on both generate-words and generate-sentences:
conlang generate-words --config sketch.json --speak-with default
conlang generate-words --config sketch.json --speak-with espeak-de5
conlang generate-words --config sketch.json --speak
--speak-with <NAME> speaks each generated item with the named voice.
The special value "default" resolves to whatever "default" is set to in the config.
--speak speaks each generated item with a randomly chosen voice from all configured voices.
This is useful when you have defined multiple voices and want to hear variety.
The two flags are mutually exclusive.
To point at a config file outside the normal search order, add --voice-config <PATH>:
conlang generate-words --config sketch.json --speak \
--voice-config ~/my-voices.json
Voice Management Commands
Three subcommands under conlang config voice help you inspect and test your setup:
List configured voices:
conlang config voice list
Shows each voice name, its driver, and key settings. Useful for verifying that your config file is being found and parsed.
Scan for available drivers and voices on the system:
conlang config voice scan
For eSpeak-ng, this shows installed standard voices and MBROLA voices (distinguishing between those with and without data files). For Polly, it queries the AWS API and lists available voice IDs for your configured region.
Test a voice with a built-in or custom phrase:
conlang config voice test espeak-de5
conlang config voice test polly-joanna "ˈka.ta ˈba.da"
When no IPA text is given, the default test phrase is ˈpa.ta ˈka.ba ˈda.ɡa ˈsa.ʃa ˈma.na – a sequence that
exercises common plosives, fricatives, and nasals.
Troubleshooting
“no voice driver compiled in” – Neither voice-espeak nor voice-polly was enabled at compile time.
Rebuild with cargo build --features voice-espeak (or voice-polly, or both).
“this voice uses the Polly driver, but Polly support was not compiled in” (or the eSpeak equivalent) –
Your config references a driver that was not enabled.
Either change the "driver" in the config or rebuild with the needed feature flag.
“no conlang configuration file found” – The tool could not find ./conlang.json or
~/.conlang/config.json.
Create one or use --voice-config <path>.
“unknown voice” – The name passed to --speak-with does not match any key in "definitions".
Run conlang config voice list to see available names.
For driver-specific issues (system libraries, MBROLA data, AWS credentials), see the eSpeak-ng and AWS Polly pages.
eSpeak-ng
eSpeak-ng is an open-source speech synthesizer that runs entirely on your machine. It produces robotic, formant-based speech – not the most natural-sounding output, but it is free, fast, and works offline, making it ideal for rapid iteration on a language’s sound.
NOTE
If you just want the configuration fields, skip to Configuration.
Setup
Enable the feature flag when building:
cargo build --features voice-espeak
The espeak-ng system library must be installed on your machine.
On Debian and Ubuntu, the package is espeak-ng-data; on other distributions, look for espeak-ng in your
package manager.
You can verify the installation by running conlang config voice scan, which lists all voices that eSpeak-ng
can find.
Configuration
Each eSpeak voice entry requires "driver": "espeak" and accepts the following optional fields:
| Field | Type | Default | Description |
|---|---|---|---|
driver | string | (required) | Must be "espeak" |
voice | string | "en" | eSpeak voice or language code (e.g., "en", "mb-de5") |
rate | integer | (none) | Speech rate |
pitch | integer | (none) | Pitch adjustment (0-99) |
volume | integer | (none) | Volume level (0-200) |
A minimal entry:
{
"driver": "espeak"
}
This uses the English voice at the default rate, pitch, and volume.
A more complete entry:
{
"driver": "espeak",
"voice": "mb-de5",
"rate": 120,
"pitch": 50,
"volume": 100
}
MBROLA Voices
Voice codes starting with mb- (like mb-de5 or mb-en1) use the MBROLA
speech synthesis system through eSpeak-ng.
MBROLA voices produce smoother, more natural output than eSpeak-ng’s default formant synthesis, but they require
separate data files to be installed on the system.
Run conlang config voice scan to see which MBROLA voices are available.
The scan distinguishes between:
- Data installed – the voice is ready to use
- Definition exists, data missing – eSpeak-ng knows about the voice, but the data package is not installed
Install the data package for the voice you want (e.g., mbrola-de5 on Debian/Ubuntu) and it will appear in the
“data installed” list on the next scan.
IPA Conversion
The toolkit generates IPA strings, but eSpeak-ng uses its own phoneme notation internally. When you speak through an eSpeak voice, the toolkit automatically converts IPA to eSpeak’s Kirshenbaum-based phoneme format.
This mapping covers the full consonant and vowel inventory described in Phonemes, including compound segments like affricates (/t͡ʃ/, /d͡ʒ/) and common diacritics (aspiration, nasalization). Some less common diacritics and tone markers have partial or approximate support – if a particular sound does not come through as expected, try a nearby phoneme or switch to AWS Polly, which accepts IPA directly.
AWS Polly
AWS Polly is a cloud-based text-to-speech service that produces natural-sounding neural speech. It accepts IPA directly through SSML, so there is no phoneme conversion step – what you write in your phonetic inventory is what Polly tries to pronounce.
NOTE
If you just want the configuration fields, skip to Configuration.
Setup
Enable the feature flag when building:
cargo build --features voice-polly
Polly requires AWS credentials. The simplest method is to set environment variables:
export AWS_ACCESS_KEY_ID=your-key
export AWS_SECRET_ACCESS_KEY=your-secret
export AWS_REGION=us-east-1
Alternatively, configure ~/.aws/credentials using the AWS CLI.
The region determines which voices are available – most voices exist in us-east-1.
You can verify that credentials are working by running conlang config voice scan, which queries the Polly API
and lists available voices for your configured region.
Configuration
Each Polly voice entry requires "driver": "polly" and accepts the following optional fields:
| Field | Type | Default | Description |
|---|---|---|---|
driver | string | (required) | Must be "polly" |
voice_id | string | "Joanna" | Polly voice identifier (e.g., "Joanna", "Matthew") |
engine | string | "neural" | "neural" for neural TTS or "standard" for classic |
A minimal entry:
{
"driver": "polly"
}
This uses the Joanna voice with the neural engine.
A multi-voice setup:
{
"polly-joanna": {
"driver": "polly",
"voice_id": "Joanna",
"engine": "neural"
},
"polly-matthew": {
"driver": "polly",
"voice_id": "Matthew",
"engine": "standard"
}
}
The "neural" engine produces higher-quality speech but is not available for every voice ID.
The "standard" engine is available for all voices and is less expensive per character.
How IPA Reaches Polly
When you speak through a Polly voice, the toolkit wraps your IPA string in an SSML <phoneme> tag:
<phoneme alphabet="ipa" ph="ˈpa.ta">.</phoneme>
Polly interprets the IPA directly, so there is no lossy conversion step. This gives Polly higher IPA fidelity than eSpeak-ng for most sounds, though Polly’s phoneme support is limited to the sounds that exist in its voice’s base language.
Discovering Voices
Run conlang config voice scan to query the Polly DescribeVoices API:
conlang config voice scan
This lists every voice available in your configured AWS region, along with the voice ID, language, and
supported engines.
Use the voice IDs from this list as the "voice_id" value in your configuration.