feat: initial commit of markovspeed p5.js presidential speech engine

This commit is contained in:
2026-11-07 12:28:46 +01:00
commit a04c3f11bc
4 changed files with 640 additions and 0 deletions
+388
View File
@@ -0,0 +1,388 @@
// =============================================================================
// 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;
}
}