Tutorial · Beginner · ~40 min
JavaScript · Markov Chains · NLP

Markov Chain Text Generator

A Markov chain generates text by remembering the last N words and randomly choosing what comes next — based on frequencies observed in a training corpus. Order-2 chains produce surprisingly coherent gibberish; order-4 chains often reproduce exact source sentences. Build one in 60 lines of JavaScript.

1Tokenise and build n-gram table

Split the training text into tokens (words, punctuation). For each n-gram (window of N consecutive tokens), record what token follows it:

function tokenize(text) { // Keep words, apostrophes, and punctuation as separate tokens return text .toLowerCase() .match(/[a-z']+|[.,!?;:"-]/g) || []; } function buildModel(text, order = 2) { const tokens = tokenize(text); const model = new Map(); // key: N-word tuple → value: array of following words for (let i = 0; i < tokens.length - order; i++) { const key = tokens.slice(i, i + order).join(' '); const next = tokens[i + order]; if (!model.has(key)) model.set(key, []); model.get(key).push(next); } return model; } // Example usage with a Shakespeare excerpt const corpus = `To be or not to be that is the question whether tis nobler in the mind to suffer the slings and arrows of outrageous fortune …`; const model = buildModel(corpus, 2); // model.get("to be") → ["or", "that", "or", "or", ...] (frequencies encoded as repetitions)
Storing frequencies as repeated values (e.g. ["the","the","the","a"]) rather than count maps makes sampling trivially simple: just pick a random index. Memory usage is slightly higher but negligible for typical corpora.

2Generate text by sampling

function generate(model, order, maxWords = 100, seed = null) { // Start from a random key, or a provided seed phrase const keys = [...model.keys()]; let current = seed ?? keys[Math.floor(Math.random() * keys.length)]; const words = current.split(' '); for (let i = 0; i < maxWords; i++) { const candidates = model.get(current); if (!candidates || candidates.length === 0) break; const next = candidates[Math.floor(Math.random() * candidates.length)]; words.push(next); // Advance the window by one word const window = words.slice(-order); current = window.join(' '); } return words.join(' '); } console.log(generate(model, 2, 80)); // → "to be that is the question whether tis nobler in the mind to // suffer the slings and arrows of outrageous fortune whether ..."

3Temperature sampling

Raw frequency sampling always favours the most common continuations. Temperature controls diversity: below 1 = more predictable, above 1 = more random:

function temperatureSample(candidates, temperature = 1.0) { if (temperature === 1.0) { return candidates[Math.floor(Math.random() * candidates.length)]; } // Count frequencies const freq = new Map(); for (const w of candidates) freq.set(w, (freq.get(w) ?? 0) + 1); // Convert to probability distribution, apply temperature const words = [...freq.keys()]; let logits = words.map(w => Math.log(freq.get(w) / candidates.length) / temperature); const maxLogit = Math.max(...logits); let probs = logits.map(l => Math.exp(l - maxLogit)); const sum = probs.reduce((a, b) => a + b, 0); probs = probs.map(p => p / sum); // Sample from distribution let r = Math.random(), cumulative = 0; for (let i = 0; i < words.length; i++) { cumulative += probs[i]; if (r <= cumulative) return words[i]; } return words[words.length - 1]; }

4Start tokens and sentence boundaries

// Add special <START> marker at sentence beginnings function buildModelWithBoundaries(text, order = 2) { const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(Boolean); const model = new Map(); for (const sentence of sentences) { const tokens = tokenize(sentence); const padded = [...Array(order).fill('<START>'), ...tokens, '<END>']; for (let i = 0; i < padded.length - order; i++) { const key = padded.slice(i, i + order).join(' '); const next = padded[i + order]; if (!model.has(key)) model.set(key, []); model.get(key).push(next); } } return model; } // Generator: always start from <START> and stop at <END> function generateSentence(model, order, temperature = 1.0) { let current = Array(order).fill('<START>').join(' '); const words = []; for (let i = 0; i < 200; i++) { const candidates = model.get(current); if (!candidates) break; const next = temperatureSample(candidates, temperature); if (next === '<END>') break; words.push(next); current = [...current.split(' ').slice(1), next].join(' '); } return words.join(' '); }

5Build a browser UI

<!-- HTML skeleton --> <textarea id="corpus" rows="8" placeholder="Paste training text…"></textarea> <label>Order: <input id="order" type="range" min="1" max="5" value="2"> <span id="orderVal">2</span></label> <label>Temperature: <input id="temp" type="range" min="1" max="20" value="10"> <span id="tempVal">1.0</span></label> <button id="train">Train</button> <button id="gen">Generate</button> <div id="output"></div> <script> let model = null, currentOrder = 2; document.getElementById('order').addEventListener('input', e => { document.getElementById('orderVal').textContent = currentOrder = +e.target.value; }); document.getElementById('temp').addEventListener('input', e => { document.getElementById('tempVal').textContent = (+e.target.value / 10).toFixed(1); }); document.getElementById('train').addEventListener('click', () => { const corpus = document.getElementById('corpus').value; model = buildModelWithBoundaries(corpus, currentOrder); document.getElementById('output').textContent = `Trained. ${model.size} unique states.`; }); document.getElementById('gen').addEventListener('click', () => { if (!model) return; const temp = +document.getElementById('temp').value / 10; const text = Array.from({length: 5}, () => generateSentence(model, currentOrder, temp)).join(' '); document.getElementById('output').textContent = text; }); </script>
Try order 1 (very random), order 3 (coherent paragraphs), and order 5 (nearly memorises the source). The sweet spot for interesting generated text is usually order 2–3 with temperature 0.8–1.2.

Frequently Asked Questions

What will I learn in this tutorial?

Build a Markov chain text generator in JavaScript — n-gram model, training on any corpus, temperature sampling, and generating coherent-sounding sentences.

What topics are covered in this tutorial?

This tutorial covers: Tokenise and build n-gram table, Generate text by sampling, Temperature sampling, Start token and sentence boundaries, Build a simple browser UI.

What tools and technologies does this tutorial use?

This tutorial uses JavaScript, Markov Chains, NLP.

How long does this tutorial take?

This tutorial takes approximately 40 minutes to complete.

What prerequisites do I need before starting?

This is a Beginner-level tutorial — no special preparation beyond basic JavaScript is assumed.