// ============================================================================= // MarkovSpeed // p5.js Interactive Presidential Markov Engine // Ported & expanded from Processing (2014-2016 / 2026) MarkovSpeech.pde // Features: Dynamic Cadence, Semantic Force-Directed Graph, Web Speech TTS // CC0 1.0 Universal / Public Domain. Author: --kof // ============================================================================= const CORPUS_PRESIDENTIAL = [ "Vážení", "spoluobčané,", "stojíme", "na", "historické", "křižovatce.", "Naše", "země", "potřebuje", "odvahu,", "rozvahu", "a", "společenský", "smír.", "Svoboda", "a", "demokracie", "nejsou", "samozřejmostí,", "jsou", "každodenním", "závazkem.", "V", "této", "chvíli", "musíme", "překonat", "všechny", "vnitřní", "spory.", "Hospodářská", "síla", "a", "technologická", "tvořivost", "tvoří", "budoucnost.", "Důvěra", "v", "instituce", "pramení", "z", "čistého", "svědomí", "a", "poctivé", "práce.", "Pravda", "a", "láska", "musí", "zvítězit", "nad", "lží", "a", "nenávistí.", "Kdo", "seje", "vítr,", "sklízí", "bouři,", "ale", "naše", "vůle", "zůstává", "pevná.", "Přeji", "vám", "pevné", "zdraví,", "naději", "a", "víru", "v", "naši", "společnou", "vlast." ]; const CORPUS_CYBERPUNK = [ "Signál", "prochází", "měděným", "vedením", "v", "rytmu", "padesáti", "hertzů.", "Autonomní", "uzly", "vyměňují", "pakety", "bez", "dohledu", "centrálních", "brokerů.", "V", "paměti", "tmpfs", "rotuje", "bezeztrátový", "kruhový", "buffer.", "Svoboda", "spočívá", "v", "vlastním", "železe,", "v", "čistém", "céčku", "a", "lokálních", "modelech.", "Žádná", "korporace", "nemůže", "vypnout", "to,", "co", "běží", "na", "baterii", "ze", "slunce.", "Tranzistory", "chladnou,", "ale", "lidská", "paměť", "zůstává", "trvalá.", "Přenášíme", "pravdu", "přes", "šum", "éteru", "až", "na", "konec", "noci." ]; let nodes = []; let particles = []; let currentState = 0; let isRunning = true; let isBursting = false; let currentCorpusIdx = 0; let baseCadence = 12; // Words per second let stepAccumulator = 0; let lastStepTime = 0; let speechHistory = []; let currentSpeechStr = ""; let totalTokensEmitted = 0; // TTS Web Speech let ttsEnabled = false; let synth = null; let czechVoice = null; // Visual layout let centerX, centerY; let radius = 260; class MarkovNode { constructor(id, token, x, y) { this.id = id; this.token = token; this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.targets = []; this.weights = []; this.flashIntensity = 0.0; } addTransition(targetId, weight) { this.targets.push(targetId); this.weights.push(weight); } selectNext() { if (this.targets.length === 0) return 0; let total = this.weights.reduce((a, b) => a + b, 0); let roll = random(total); let accum = 0; for (let i = 0; i < this.targets.length; i++) { accum += this.weights[i]; if (roll <= accum) return this.targets[i]; } return this.targets[0]; } } class Particle { constructor(startX, startY, targetX, targetY, targetId) { this.x = startX; this.y = startY; this.targetX = targetX; this.targetY = targetY; this.targetId = targetId; this.progress = 0.0; this.speed = random(0.08, 0.16); } update() { this.progress += this.speed; return this.progress >= 1.0; } render() { let px = lerp(this.x, this.targetX, this.progress); let py = lerp(this.y, this.targetY, this.progress); noStroke(); fill(255, 204, 17, (1.0 - this.progress) * 255); circle(px, py, 5); } } function buildModel(vocab) { nodes = []; particles = []; currentState = 0; let total = vocab.length; for (let i = 0; i < total; i++) { // Spatial layout: Fibonacci phyllotaxis spiral let angle = i * 2.39996323; // Golden angle let r = sqrt(i / total) * radius; let x = centerX + cos(angle) * r; let y = centerY + sin(angle) * r; nodes.push(new MarkovNode(i, vocab[i], x, y)); } // Generate transition probabilities for (let i = 0; i < total; i++) { let n = nodes[i]; // Main sequential train if (i + 1 < total) { n.addTransition(i + 1, 0.65); } else { n.addTransition(0, 0.70); // loop } // Branching rhetorical attributions let b1 = (i + 3) % total; let b2 = (i + 7) % total; let b3 = (i + 13) % total; n.addTransition(b1, 0.18); n.addTransition(b2, 0.11); n.addTransition(b3, 0.06); } } function setup() { let container = document.getElementById("canvas-container"); let canvas = createCanvas(container.clientWidth, container.clientHeight); canvas.parent("canvas-container"); centerX = width / 2; centerY = height / 2; radius = min(width, height) * 0.40; buildModel(CORPUS_PRESIDENTIAL); initTTS(); initUI(); } function windowResized() { let container = document.getElementById("canvas-container"); resizeCanvas(container.clientWidth, container.clientHeight); centerX = width / 2; centerY = height / 2; radius = min(width, height) * 0.40; buildModel(currentCorpusIdx === 0 ? CORPUS_PRESIDENTIAL : CORPUS_CYBERPUNK); } function initTTS() { if ("speechSynthesis" in window) { synth = window.speechSynthesis; let loadVoices = () => { let voices = synth.getVoices(); czechVoice = voices.find(v => v.lang.startsWith("cs")) || voices[0]; }; loadVoices(); if (synth.onvoiceschanged !== undefined) { synth.onvoiceschanged = loadVoices; } } } function speakWord(word) { if (!ttsEnabled || !synth) return; // Clean punctuation let clean = word.replace(/[.,!?;:]/g, ""); if (!clean) return; let utter = new SpeechSynthesisUtterance(clean); if (czechVoice) utter.voice = czechVoice; utter.lang = "cs-CZ"; // Adjust rate to current speed let rate = map(baseCadence, 1, 60, 0.85, 2.5); utter.rate = constrain(rate, 0.5, 3.0); utter.pitch = 1.0; utter.volume = 0.9; synth.speak(utter); } function initUI() { const btnToggle = document.getElementById("btn-toggle"); btnToggle.addEventListener("click", () => { isRunning = !isRunning; btnToggle.classList.toggle("active", isRunning); btnToggle.textContent = isRunning ? "PAUZA / START" : "ZASTAVENO"; }); const btnBurst = document.getElementById("btn-burst"); btnBurst.addEventListener("mousedown", () => { isBursting = true; }); btnBurst.addEventListener("mouseup", () => { isBursting = false; }); btnBurst.addEventListener("mouseleave", () => { isBursting = false; }); btnBurst.addEventListener("touchstart", (e) => { e.preventDefault(); isBursting = true; }); btnBurst.addEventListener("touchend", () => { isBursting = false; }); const btnTTS = document.getElementById("btn-tts"); btnTTS.addEventListener("click", () => { ttsEnabled = !ttsEnabled; btnTTS.classList.toggle("active", ttsEnabled); btnTTS.textContent = ttsEnabled ? "TTS HLAS: ZAP" : "TTS HLAS: VYP"; if (ttsEnabled && synth) { speakWord("Techné aktivováno"); } }); const btnCorpus = document.getElementById("btn-corpus"); btnCorpus.addEventListener("click", () => { currentCorpusIdx = 1 - currentCorpusIdx; let isPres = (currentCorpusIdx === 0); btnCorpus.textContent = isPres ? "KORPUS: PREZIDENTSKÝ" : "KORPUS: CYBERPUNK"; buildModel(isPres ? CORPUS_PRESIDENTIAL : CORPUS_CYBERPUNK); currentSpeechStr = "> "; updateTicker(); }); const speedSlider = document.getElementById("speed-slider"); const speedVal = document.getElementById("speed-val"); speedSlider.addEventListener("input", (e) => { baseCadence = parseInt(e.target.value); speedVal.textContent = baseCadence + " W/S"; }); } function keyPressed() { if (key === " ") { isBursting = true; return false; } } function keyReleased() { if (key === " ") { isBursting = false; return false; } } function emitToken() { if (nodes.length === 0) return; let current = nodes[currentState]; current.flashIntensity = 1.0; totalTokensEmitted++; let token = current.token; speakWord(token); // Update speech text currentSpeechStr += token + " "; if (currentSpeechStr.length > 280) { currentSpeechStr = "> ..." + currentSpeechStr.substring(currentSpeechStr.length - 220); } updateTicker(token); // Pick next state let nextState = current.selectNext(); let nextNode = nodes[nextState]; // Spawn visual packet particle particles.push(new Particle(current.x, current.y, nextNode.x, nextNode.y, nextState)); currentState = nextState; } function updateTicker(lastToken = "") { let elem = document.getElementById("speech-text"); if (elem) elem.textContent = currentSpeechStr; let tag = document.getElementById("token-val"); if (tag && lastToken) tag.textContent = lastToken; let tempo = document.getElementById("tempo-val"); let effectiveCadence = isBursting ? baseCadence * 3 : baseCadence; if (tempo) tempo.textContent = (effectiveCadence * 60) + " WPM"; let entropy = document.getElementById("entropy-val"); if (entropy) { let curr = nodes[currentState]; let H = curr ? (Math.log2(curr.targets.length) || 1.0).toFixed(2) : "2.41"; entropy.textContent = `H = ${H} bit`; } } function draw() { background(5, 8, 12, 45); // Phosphor persistence decay // Center subtle cortical oscillation ring let pulse = sin(frameCount * 0.05) * 6; noFill(); stroke(26, 42, 58, 80); circle(centerX, centerY, (radius * 2) + pulse); // 1. Draw static and active transition lines for (let i = 0; i < nodes.length; i++) { let n = nodes[i]; for (let tId of n.targets) { let target = nodes[tId]; if (!target) continue; let isCurr = (i === currentState); if (isCurr) { stroke(255, 204, 17, 180); strokeWeight(1.5); } else { stroke(20, 36, 50, 40); strokeWeight(0.7); } line(n.x, n.y, target.x, target.y); } } // 2. Update and draw particles for (let i = particles.length - 1; i >= 0; i--) { let p = particles[i]; p.render(); if (p.update()) { particles.splice(i, 1); } } // 3. Draw nodes & labels for (let i = 0; i < nodes.length; i++) { let n = nodes[i]; n.flashIntensity *= 0.91; // Decay let isCurrent = (i === currentState); let glow = n.flashIntensity; if (isCurrent || glow > 0.05) { fill(255, 204, 17, 180 + glow * 75); stroke(255, 230, 80, glow * 255); strokeWeight(2); circle(n.x, n.y, 10 + glow * 8); // Glowing text label noStroke(); fill(255, 240, 160); textSize(12); textAlign(CENTER, BOTTOM); text(n.token, n.x, n.y - 10); } else { fill(35, 55, 75); noStroke(); circle(n.x, n.y, 4); fill(90, 120, 145, 160); textSize(9); textAlign(CENTER, TOP); text(n.token, n.x, n.y + 6); } } // 4. Cadence timing engine if (isRunning) { let effectiveCadence = isBursting ? baseCadence * 3 : baseCadence; let intervalSec = 1.0 / effectiveCadence; let now = millis() / 1000.0; if (lastStepTime === 0) lastStepTime = now; let dt = now - lastStepTime; lastStepTime = now; stepAccumulator += dt; if (stepAccumulator >= intervalSec) { emitToken(); stepAccumulator = 0; } } else { lastStepTime = millis() / 1000.0; } }