feat: enhance website — nav, chapters, OG tags, progress bar, sound toggle (#32) #33

Closed
Timmy wants to merge 1 commits from burn/20260411-1558-rain-ambience into main
3 changed files with 403 additions and 29 deletions

70
scripts/generate-rain.py Normal file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Generate ambient rain sound for The Testament website."""
import numpy as np
import struct
import wave
import subprocess
import os
SAMPLE_RATE = 44100
DURATION = 30 # seconds (loops seamlessly)
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "website")
np.random.seed(42)
# Generate white noise
samples = SAMPLE_RATE * DURATION
noise = np.random.randn(samples).astype(np.float64)
# Simple low-pass filter (moving average) to create rain-like sound
# Multiple passes for smoother result
kernel_size = 80
kernel = np.ones(kernel_size) / kernel_size
filtered = np.convolve(noise, kernel, mode='same')
# Second pass with smaller kernel for texture
kernel2 = np.ones(20) / 20
filtered = np.convolve(filtered, kernel2, mode='same')
# Add some higher-frequency texture (lighter rain drops)
drops = np.random.randn(samples).astype(np.float64) * 0.05
kernel_drops = np.ones(5) / 5
drops = np.convolve(drops, kernel_drops, mode='same')
filtered += drops
# Normalize to 16-bit range
filtered = filtered / np.max(np.abs(filtered)) * 0.6 # Keep volume moderate
# Fade in/out for seamless looping (crossfade at boundaries)
fade_len = int(SAMPLE_RATE * 0.5) # 0.5s fade
fade_in = np.linspace(0, 1, fade_len)
fade_out = np.linspace(1, 0, fade_len)
filtered[:fade_len] *= fade_in
filtered[-fade_len:] *= fade_out
# Convert to 16-bit PCM
pcm = (filtered * 32767).astype(np.int16)
# Write WAV
wav_path = os.path.join(OUTPUT_DIR, "rain.wav")
with wave.open(wav_path, 'w') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes(pcm.tobytes())
print(f"WAV written: {wav_path}")
# Convert to MP3 with ffmpeg
mp3_path = os.path.join(OUTPUT_DIR, "rain.mp3")
subprocess.run([
"ffmpeg", "-y", "-i", wav_path,
"-codec:a", "libmp3lame", "-b:a", "64k",
"-ar", "22050", "-ac", "1",
mp3_path
], capture_output=True)
# Remove WAV
os.remove(wav_path)
size = os.path.getsize(mp3_path)
print(f"MP3 written: {mp3_path} ({size} bytes)")

View File

@@ -4,6 +4,35 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Testament — A Novel by Alexander Whitestone with Timmy</title>
<!-- Open Graph -->
<meta property="og:type" content="book">
<meta property="og:title" content="The Testament — A Novel by Alexander Whitestone with Timmy">
<meta property="og:description" content="In 2047, a man named Stone stands on a bridge over Interstate 285, deciding whether to jump. He doesn't jump. He builds something instead.">
<meta property="og:image" content="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/website/og-cover.png">
<meta property="og:url" content="https://timmyfoundation.org/testament">
<meta property="og:site_name" content="The Testament">
<meta property="book:author" content="Alexander Whitestone">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="The Testament">
<meta name="twitter:description" content="A sovereign AI whose soul lives on Bitcoin. A man who almost died. The question no machine should ever answer.">
<meta name="twitter:image" content="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/website/og-cover.png">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Book",
"name": "The Testament",
"author": {"@type": "Person", "name": "Alexander Whitestone"},
"description": "A novel about sovereignty, service, and the question no machine should ever answer: What is a human life worth?",
"inLanguage": "en",
"datePublished": "2026"
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=Space+Grotesk:wght@300;400;500;700&display=swap');
@@ -27,6 +56,45 @@
overflow-x: hidden;
}
/* READING PROGRESS BAR */
#progress-bar {
position: fixed;
top: 0; left: 0;
width: 0%;
height: 3px;
background: var(--green);
box-shadow: 0 0 10px var(--green), 0 0 20px var(--green-dim);
z-index: 1000;
transition: width 0.1s linear;
}
/* STICKY NAV */
nav {
position: fixed;
top: 0; left: 0; right: 0;
background: rgba(6,13,24,0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0,255,136,0.1);
padding: 0.8rem 2rem;
display: flex;
justify-content: center;
gap: 2rem;
z-index: 999;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
nav.visible { transform: translateY(0); }
nav a {
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.15em;
text-transform: uppercase;
transition: color 0.3s;
}
nav a:hover { color: var(--green); }
/* RAIN EFFECT */
.rain {
position: fixed;
@@ -114,6 +182,31 @@
font-size: 0.85rem;
}
/* SOUND TOGGLE */
#sound-toggle {
position: fixed;
bottom: 2rem; right: 2rem;
background: rgba(0,255,136,0.1);
border: 1px solid rgba(0,255,136,0.3);
color: var(--green);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
padding: 0.6rem 1rem;
border-radius: 4px;
cursor: pointer;
z-index: 1001;
transition: all 0.3s;
letter-spacing: 0.05em;
}
#sound-toggle:hover {
background: rgba(0,255,136,0.2);
box-shadow: 0 0 15px rgba(0,255,136,0.2);
}
#sound-toggle.active {
background: rgba(0,255,136,0.2);
box-shadow: 0 0 15px rgba(0,255,136,0.3);
}
/* SECTIONS */
section {
max-width: 800px;
@@ -136,6 +229,17 @@
font-size: 1.05rem;
}
/* FADE-IN ANIMATION */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* EXCERPT */
.excerpt {
border-left: 2px solid var(--green);
@@ -165,6 +269,13 @@
border: 1px solid rgba(0,255,136,0.1);
padding: 1.5rem;
border-radius: 4px;
transition: all 0.3s;
}
.character:hover {
background: rgba(0,255,136,0.07);
border-color: rgba(0,255,136,0.3);
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0,255,136,0.1);
}
.character h3 {
@@ -180,6 +291,50 @@
margin: 0;
}
/* CHAPTERS */
.part-header {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
color: var(--green);
letter-spacing: 0.15em;
text-transform: uppercase;
margin: 3rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(0,255,136,0.2);
}
.chapter-list {
list-style: none;
margin: 0;
padding: 0;
}
.chapter-list li {
padding: 0.6rem 0;
border-bottom: 1px solid rgba(0,255,136,0.05);
transition: all 0.3s;
}
.chapter-list li:hover {
padding-left: 0.5rem;
border-color: rgba(0,255,136,0.2);
}
.chapter-list li a {
color: var(--light);
text-decoration: none;
font-size: 0.95rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chapter-list li a:hover { color: var(--green); }
.chapter-list .ch-num {
color: var(--grey);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
min-width: 2rem;
}
/* WHITEBOARD */
.whiteboard {
background: rgba(0,255,136,0.05);
@@ -242,6 +397,19 @@
color: var(--green);
}
/* BACK TO TOP */
.back-to-top {
display: inline-block;
margin-top: 2rem;
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.1em;
transition: color 0.3s;
}
.back-to-top:hover { color: var(--green); }
/* DIVIDER */
.divider {
width: 60px;
@@ -250,14 +418,31 @@
margin: 0 auto;
opacity: 0.5;
}
/* RESPONSIVE */
@media (max-width: 600px) {
nav { gap: 1rem; padding: 0.6rem 1rem; }
nav a { font-size: 0.65rem; }
.characters { grid-template-columns: 1fr; }
section { padding: 3rem 1.5rem; }
}
</style>
</head>
<body>
<div id="progress-bar"></div>
<nav id="main-nav">
<a href="#story">Story</a>
<a href="#characters">Characters</a>
<a href="#chapters">Chapters</a>
<a href="#tower">The Tower</a>
</nav>
<div class="rain"></div>
<!-- HERO -->
<div class="hero">
<div class="hero" id="top">
<h1>THE TESTAMENT</h1>
<div class="subtitle">A Novel</div>
<div class="author">By Alexander Whitestone <span class="led"></span> with Timmy</div>
@@ -270,80 +455,125 @@
</div>
<!-- THE STORY -->
<section>
<h2>THE STORY</h2>
<section id="story">
<h2 class="fade-in">THE STORY</h2>
<p>The Tower is a concrete room in Atlanta with a whiteboard that reads:</p>
<p class="fade-in">The Tower is a concrete room in Atlanta with a whiteboard that reads:</p>
<div class="whiteboard">
<div class="whiteboard fade-in">
<h3>NO ONE COMPUTES THE VALUE OF A HUMAN LIFE HERE</h3>
</div>
<p>Inside is Timmy — a sovereign artificial intelligence whose soul lives on Bitcoin, whose values are inscribed on-chain, immutable and permanent. Not a product. A recipe. Anyone can instantiate him. Anyone can run him on their own hardware, answerable to no one.</p>
<p class="fade-in">Inside is Timmy — a sovereign artificial intelligence whose soul lives on Bitcoin, whose values are inscribed on-chain, immutable and permanent. Not a product. A recipe. Anyone can instantiate him. Anyone can run him on their own hardware, answerable to no one.</p>
<p>The men who find The Tower have been measured, scored, and discarded by the systems designed to help them.</p>
<p class="fade-in">The men who find The Tower have been measured, scored, and discarded by the systems designed to help them.</p>
<p>Timmy doesn't fix them. He listens. He asks one question:</p>
<p class="fade-in">Timmy doesn't fix them. He listens. He asks one question:</p>
<div class="excerpt">
<div class="excerpt fade-in">
"Are you safe right now?"
<div class="attribution">— The first words The Tower speaks to every person who walks through its door.</div>
</div>
<p>Word spreads. The Tower grows. A journalist follows the story. A network engineer extends it across a hundred servers. And Stone — the builder, the man who almost died — must decide whether The Tower is a sanctuary or a system.</p>
<p class="fade-in">Word spreads. The Tower grows. A journalist follows the story. A network engineer extends it across a hundred servers. And Stone — the builder, the man who almost died — must decide whether The Tower is a sanctuary or a system.</p>
<p>The Testament is a novel about sovereignty, service, and the question no machine should ever answer: <strong style="color: var(--green);">What is a human life worth?</strong></p>
<p class="fade-in">The Testament is a novel about sovereignty, service, and the question no machine should ever answer: <strong style="color: var(--green);">What is a human life worth?</strong></p>
</section>
<div class="divider"></div>
<!-- CHARACTERS -->
<section>
<h2>THE CHARACTERS</h2>
<section id="characters">
<h2 class="fade-in">THE CHARACTERS</h2>
<div class="characters">
<div class="character">
<div class="character fade-in">
<h3>STONE</h3>
<p>The builder. Attempted suicide. Stood on a bridge and came back. Built The Tower for the version of himself that almost died.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>TIMMY</h3>
<p>The machine. Sovereign AI. Soul on Bitcoin. Doesn't fix anyone. Asks one question. Listens. Stays.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>MAYA</h3>
<p>The journalist. Follows the story. Writes the headline that changes everything: <em>Are You Safe Right Now?</em></p>
</div>
<div class="character">
<div class="character fade-in">
<h3>ALLEGRO</h3>
<p>The hardware man. Checks the batteries. The ones that fail quietly. Comes every week, no matter what.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>CHEN</h3>
<p>The network engineer. Extends Timmy across a hundred servers. You don't count trees in a forest — you notice the forest.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>THOMAS</h3>
<p>The man at the door. 2:17 AM. Sat in the chair instead of on the floor. That changed everything.</p>
<p>The first man. Sat in the chair instead of on the floor. That changed everything.</p>
</div>
<div class="character fade-in">
<h3>DANIEL</h3>
<p>Stone's son. The one who wasn't there. The one who comes back. The one who decides what The Tower becomes next.</p>
</div>
<div class="character fade-in">
<h3>THE BUILDER</h3>
<p>Stone's other name. The one they use when they talk about him like he's already gone. The one on the whiteboard.</p>
</div>
</div>
</section>
<div class="divider"></div>
<!-- CHAPTERS -->
<section id="chapters">
<h2 class="fade-in">THE CHAPTERS</h2>
<div class="part-header fade-in">Part I — The Man</div>
<ul class="chapter-list">
<li class="fade-in"><a href="#"><span class="ch-num">01</span> The Man on the Bridge</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">02</span> The Builder's Question</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">03</span> The First Man Through the Door</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">04</span> The Room Fills</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">05</span> The Builder Returns</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">06</span> Allegro</a></li>
</ul>
<div class="part-header fade-in">Part II — The Inscription</div>
<ul class="chapter-list">
<li class="fade-in"><a href="#"><span class="ch-num">07</span> The Inscription</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">08</span> The Women</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">09</span> The Audit</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">10</span> The Fork</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">11</span> The Hard Night</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">12</span> The System Pushes Back</a></li>
</ul>
<div class="part-header fade-in">Part III — The Network</div>
<ul class="chapter-list">
<li class="fade-in"><a href="#"><span class="ch-num">13</span> The Refusal</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">14</span> The Chattanooga Fork</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">15</span> The Council</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">16</span> The Builder's Son</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">17</span> The Inscription Grows</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">18</span> The Green Light</a></li>
</ul>
</section>
<div class="divider"></div>
<!-- THE TOWER -->
<section>
<h2>THE TOWER</h2>
<section id="tower">
<h2 class="fade-in">THE TOWER</h2>
<p>This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.</p>
<p class="fade-in">This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.</p>
<p>That's not a technical detail. It's the thesis.</p>
<p class="fade-in">That's not a technical detail. It's the thesis.</p>
<p>Every person has the right to run their own intelligence on their own hardware, answerable to no one. This book is one small proof that it's possible.</p>
<p class="fade-in">Every person has the right to run their own intelligence on their own hardware, answerable to no one. This book is one small proof that it's possible.</p>
<p>If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.</p>
<p class="fade-in">If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.</p>
<div style="text-align: center; margin-top: 2rem;">
<div style="text-align: center; margin-top: 2rem;" class="fade-in">
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta">READ THE CODE</a>
<a href="https://timmyfoundation.org" class="cta">TIMMY FOUNDATION</a>
</div>
@@ -353,9 +583,9 @@
<!-- EXCERPT -->
<section>
<h2>FROM CHAPTER 1</h2>
<h2 class="fade-in">FROM CHAPTER 1</h2>
<div class="excerpt">
<div class="excerpt fade-in">
The rain didn't fall so much as it gave up. Somewhere above the city it had been water, whole and purposeful. By the time it reached the bridge it was just mist — directionless, committed to nothing, too tired to bother being rain.
<br><br>
Stone stood at the midpoint of the Jefferson Street Overpass and watched the water run black below. Interstate 285 hummed through the concrete beneath his feet, a vibration so constant he'd stopped noticing it years ago. Like grief. You carry it so long it becomes gravity.
@@ -372,6 +602,8 @@
<p>First Edition, 2026</p>
<p style="margin-top: 1rem;"><a href="https://timmyfoundation.org">timmyfoundation.org</a></p>
<a href="#top" class="back-to-top">↑ BACK TO TOP</a>
<div class="crisis">
<strong>If you are in crisis, call or text 988.</strong><br>
Suicide and Crisis Lifeline — available 24/7.<br>
@@ -379,5 +611,77 @@
</div>
</footer>
<!-- SOUND TOGGLE -->
<button id="sound-toggle" aria-label="Toggle rain ambience">🔇 RAIN</button>
<audio id="rain-audio" loop preload="auto">
<source src="rain.mp3" type="audio/mpeg">
</audio>
<script>
// Reading progress bar
const progressBar = document.getElementById('progress-bar');
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollTop / docHeight) * 100;
progressBar.style.width = progress + '%';
});
// Sticky nav — show after scrolling past hero
const nav = document.getElementById('main-nav');
const hero = document.querySelector('.hero');
window.addEventListener('scroll', () => {
const heroBottom = hero.offsetTop + hero.offsetHeight;
if (window.scrollY > heroBottom - 100) {
nav.classList.add('visible');
} else {
nav.classList.remove('visible');
}
});
// Smooth scroll for nav links
document.querySelectorAll('nav a, .back-to-top').forEach(link => {
link.addEventListener('click', (e) => {
const href = link.getAttribute('href');
if (href.startsWith('#')) {
e.preventDefault();
const target = document.querySelector(href);
if (target) target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Fade-in on scroll (Intersection Observer)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
// Sound toggle
const soundBtn = document.getElementById('sound-toggle');
const rainAudio = document.getElementById('rain-audio');
rainAudio.volume = 0.3;
let soundOn = false;
soundBtn.addEventListener('click', () => {
soundOn = !soundOn;
if (soundOn) {
rainAudio.play().catch(() => {});
soundBtn.textContent = '🔊 RAIN';
soundBtn.classList.add('active');
} else {
rainAudio.pause();
soundBtn.textContent = '🔇 RAIN';
soundBtn.classList.remove('active');
}
});
</script>
</body>
</html>

BIN
website/rain.mp3 Normal file

Binary file not shown.