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
+56
View File
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MarkovSpeed // One-Button Presidential Speech (p5.js)</title>
<link rel="stylesheet" href="style.css">
<script src="p5.min.js"></script>
</head>
<body>
<div id="container">
<header id="header">
<div class="title-group">
<h1>MARKOVSPEED <span class="badge">1906 / 2014 / 2026</span></h1>
<div class="subtitle">ONE-BUTTON PRESIDENTIAL SPEECH ENGINE &bull; DYNAMIC CORTICAL CADENCE</div>
</div>
<div class="controls-top">
<button id="btn-toggle" class="btn active">PAUZA / START</button>
<button id="btn-burst" class="btn burst">BURST (MEZERNÍK)</button>
<button id="btn-tts" class="btn">TTS HLAS: VYP</button>
<button id="btn-corpus" class="btn">KORPUS: PREZIDENTSKÝ</button>
</div>
</header>
<main id="canvas-container"></main>
<footer id="footer">
<div id="hud-top">
<div class="metric">
<span class="label">RYCHLOST:</span>
<input type="range" id="speed-slider" min="1" max="60" value="12">
<span id="speed-val">12 W/S</span>
</div>
<div class="metric">
<span class="label">KADENCE:</span>
<span id="tempo-val" class="highlight">180 WPM</span>
</div>
<div class="metric">
<span class="label">ENTROPIE STAVŮ:</span>
<span id="entropy-val">H = 2.41 bit</span>
</div>
<div class="metric">
<span class="label">AKTIVNÍ TOKEN:</span>
<span id="token-val" class="token-tag">—</span>
</div>
</div>
<div id="ticker">
<div class="ticker-header">&gt; GENERATORSKY VYSTUP // PREZIDENTSKY PROJEV:</div>
<div id="speech-text" class="ticker-content">Čekám na signál...</div>
</div>
</footer>
</div>
<script src="sketch.js"></script>
</body>
</html>
Vendored
+2
View File
File diff suppressed because one or more lines are too long
+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;
}
}
+194
View File
@@ -0,0 +1,194 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #05080c;
color: #e0e8f0;
font-family: "IBM Plex Mono", "Terminus", "Courier New", monospace;
overflow: hidden;
height: 100vh;
width: 100vw;
user-select: none;
}
#container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
#header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 18px;
background: #090e14;
border-bottom: 2px solid #1a2634;
z-index: 10;
}
.title-group h1 {
font-size: 1.15rem;
letter-spacing: 2px;
color: #ffcc11;
font-weight: 700;
}
.title-group .badge {
font-size: 0.7rem;
background: #1e2d3d;
color: #00d4ff;
padding: 2px 6px;
border-radius: 3px;
margin-left: 8px;
vertical-align: middle;
}
.subtitle {
font-size: 0.68rem;
color: #7b93a8;
letter-spacing: 1px;
margin-top: 2px;
}
.controls-top {
display: flex;
gap: 8px;
}
.btn {
background: #0d1520;
border: 1px solid #2a3b4c;
color: #8da4be;
font-family: inherit;
font-size: 0.75rem;
padding: 6px 14px;
cursor: pointer;
letter-spacing: 1px;
transition: all 0.15s ease;
}
.btn:hover {
background: #182535;
color: #ffffff;
border-color: #ffcc11;
}
.btn.active {
background: #122822;
border-color: #33ff88;
color: #33ff88;
}
.btn.burst {
background: #2b220d;
border-color: #ffaa00;
color: #ffcc11;
}
.btn.burst:active {
background: #ffcc11;
color: #05080c;
}
#canvas-container {
flex: 1;
position: relative;
overflow: hidden;
background: #05080c;
}
#canvas-container canvas {
display: block;
}
#footer {
background: #090e14;
border-top: 2px solid #1a2634;
padding: 10px 18px;
z-index: 10;
}
#hud-top {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
font-size: 0.75rem;
padding-bottom: 8px;
border-bottom: 1px dashed #1a2634;
}
.metric {
display: flex;
align-items: center;
gap: 8px;
}
.metric .label {
color: #61788d;
}
.metric .highlight {
color: #33ff88;
font-weight: bold;
}
.metric .token-tag {
color: #ffcc11;
font-weight: bold;
background: #1f1d10;
padding: 1px 8px;
border: 1px solid #ffcc1144;
}
#speed-slider {
accent-color: #ffcc11;
width: 110px;
cursor: pointer;
}
#speed-val {
color: #ffcc11;
min-width: 48px;
}
#ticker {
margin-top: 8px;
}
.ticker-header {
font-size: 0.65rem;
color: #ffaa00;
letter-spacing: 1.5px;
margin-bottom: 4px;
}
.ticker-content {
font-size: 0.95rem;
line-height: 1.45;
color: #d8e8f8;
min-height: 48px;
max-height: 68px;
overflow-y: auto;
word-break: break-word;
background: #040608;
padding: 6px 10px;
border-left: 3px solid #ffcc11;
}
.ticker-content::after {
content: "█";
color: #ffcc11;
animation: blink 0.8s infinite;
margin-left: 4px;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}