initial commit
This commit is contained in:
commit
33c9b16267
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
poetry.lock
|
||||
|
||||
# Generated files
|
||||
**/*.mp4
|
||||
0
cipher/__init__.py
Normal file
0
cipher/__init__.py
Normal file
BIN
cipher/assets/poacher_intro.wav
Normal file
BIN
cipher/assets/poacher_intro.wav
Normal file
Binary file not shown.
BIN
cipher/assets/static.wav
Normal file
BIN
cipher/assets/static.wav
Normal file
Binary file not shown.
8
cipher/data/daybook_master_example.txt
Normal file
8
cipher/data/daybook_master_example.txt
Normal file
@ -0,0 +1,8 @@
|
||||
=== NERDLETTER DAY BOOK MASTER — ISSUE 01 (JUN 2026) ===
|
||||
|
||||
PAGE 001 NERDLETTER DAY BOOK #001
|
||||
VLGKU JXCYH LYMDC ETLUB ZIAUI OOABL RSPXP SEILP LFOOF FQFRY
|
||||
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
|
||||
=== END OF MASTER PAD — 1 PAGES GENERATED ===
|
||||
51
cipher/encrypt.py
Normal file
51
cipher/encrypt.py
Normal file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_pad(filename="cipher/data/daybook_master_example.txt"):
|
||||
"""Load the master pad file."""
|
||||
pad_text = Path(filename).read_text()
|
||||
# Extract only the 5-letter groups, ignore headers and spaces
|
||||
groups = [g for g in pad_text.split() if len(g) == 5 and g.isalpha()]
|
||||
return "".join(groups).upper()
|
||||
|
||||
|
||||
def encrypt_message(plaintext: str, pad: str, start_pos: int):
|
||||
"""Encrypt using the pad starting at a given position."""
|
||||
# Clean plaintext: uppercase letters only, remove everything else
|
||||
pt = "".join(c for c in plaintext.upper() if c.isalpha())
|
||||
if not pt:
|
||||
return "ERROR: No letters in message"
|
||||
|
||||
ciphertext = []
|
||||
for i, char in enumerate(pt):
|
||||
if i + start_pos >= len(pad):
|
||||
return "ERROR: Pad exhausted! Generate new master."
|
||||
p = ord(pad[start_pos + i]) - 65
|
||||
c = ord(char) - 65
|
||||
d = (c + p) % 26
|
||||
ciphertext.append(chr(d + 65))
|
||||
|
||||
# Format in 5-letter groups
|
||||
ct_str = "".join(ciphertext)
|
||||
return " ".join(ct_str[j:j + 5] for j in range(0, len(ct_str), 5))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 encrypt.py 'Your secret message here' [start_page]")
|
||||
print("Example: python3 encrypt.py 'Meeting at the retro show on Saturday' 7")
|
||||
sys.exit(1)
|
||||
|
||||
message = sys.argv[1]
|
||||
start_page = int(sys.argv[2]) if len(sys.argv) > 2 else 1
|
||||
|
||||
pad_str = load_pad()
|
||||
# Each page = 50 letters (10 groups × 5)
|
||||
start_pos = (start_page - 1) * 50
|
||||
|
||||
ct = encrypt_message(message, pad_str, start_pos)
|
||||
print(f"\n=== NERDLETTER SECRET MESSAGE (Use Day Book PAGE {start_page:03d}) ===")
|
||||
print(ct)
|
||||
print("\nCopy the line above to the website.")
|
||||
23
cipher/instructions.md
Normal file
23
cipher/instructions.md
Normal file
@ -0,0 +1,23 @@
|
||||
## HOW TO DECRYPT — STEP BY STEP (using your 8-bit machine)
|
||||
|
||||
1. Type in the NERDLETTER DECRYPTOR BASIC program (see below)
|
||||
2. RUN
|
||||
3. When it asks, type the PAD groups from the appropriate page:
|
||||
|
||||
`VLGKU` then `JXCYH` then `LYMDC` then `ETLUB`
|
||||
|
||||
4. Then type the CIPHERTEXT groups:
|
||||
|
||||
`HLXIB` then `JACJP` then `ERXHN` then `EFM`
|
||||
|
||||
5. The machine will instantly print the plaintext!
|
||||
|
||||
## RESULT YOU SHOULD SEE:
|
||||
|
||||
```
|
||||
MARYHADALITTLELAMB
|
||||
```
|
||||
|
||||
(Add spaces by hand: “Mary had a little lamb”)
|
||||
|
||||
This is perfect secrecy — no computer on Earth can read it without this exact Day Book page.
|
||||
0
cipher/output/.placeholder
Normal file
0
cipher/output/.placeholder
Normal file
122
cipher/scripts/nerdletter_numbers_station.sh
Executable file
122
cipher/scripts/nerdletter_numbers_station.sh
Executable file
@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
# NERDLETTER NUMBERS STATION MP4 GENERATOR
|
||||
# Lincolnshire Poacher style with letter-by-letter cipher readout
|
||||
#
|
||||
# Audio structure:
|
||||
# Base layer (full duration): low-volume shortwave static
|
||||
# On top, in sequence:
|
||||
# 1. Lincolnshire Poacher theme
|
||||
# 2. Voice: announcement + letters (0.5s between letters, 1.0s between groups)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ASSETS_DIR="${SCRIPT_DIR}/../assets"
|
||||
|
||||
MESSAGE="$1" # e.g. "HLXIB JACJP ERXHN EFM"
|
||||
PAGE="$2" # e.g. 001
|
||||
EPISODE="${3:-01}"
|
||||
|
||||
OUTFILE="nerdletter_secret_${EPISODE}_page${PAGE}.mp4"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
SAMPLE_RATE=22050
|
||||
|
||||
echo "=== NERDLETTER SECRET TRANSMISSION #${EPISODE} ==="
|
||||
echo "Page ${PAGE} — Generating MP4..."
|
||||
|
||||
# --- 1. Generate voice announcement ---
|
||||
INTRO_TEXT="NERDLETTER SECRET MESSAGE ${EPISODE}. USE DAY BOOK PAGE ${PAGE}."
|
||||
echo "$INTRO_TEXT" | \
|
||||
espeak-ng -ven+f3 -s88 -k50 -a 200 -w "${TMPDIR}/intro.wav" --punct="" 2>/dev/null
|
||||
|
||||
# --- 2. Generate letter-by-letter audio with proper gaps ---
|
||||
# Build an ffmpeg concat list: each letter as a wav, with silence between
|
||||
CONCAT_LIST="${TMPDIR}/concat.txt"
|
||||
> "$CONCAT_LIST"
|
||||
PART=0
|
||||
|
||||
# Pre-generate silence files
|
||||
ffmpeg -f lavfi -i "anullsrc=r=${SAMPLE_RATE}:cl=mono" \
|
||||
-t 0.45 -c:a pcm_s16le "${TMPDIR}/gap_letter.wav" -y 2>/dev/null
|
||||
ffmpeg -f lavfi -i "anullsrc=r=${SAMPLE_RATE}:cl=mono" \
|
||||
-t 0.9 -c:a pcm_s16le "${TMPDIR}/gap_group.wav" -y 2>/dev/null
|
||||
|
||||
for group in $MESSAGE; do
|
||||
echo " Group: $group"
|
||||
for ((i=0; i<${#group}; i++)); do
|
||||
letter="${group:$i:1}"
|
||||
PART_FILE="${TMPDIR}/letter_${PART}.wav"
|
||||
echo "$letter" | espeak-ng -ven+f3 -s81 -k65 -a 210 -w "$PART_FILE" 2>/dev/null
|
||||
echo "file '${PART_FILE}'" >> "$CONCAT_LIST"
|
||||
PART=$((PART + 1))
|
||||
|
||||
# 0.5s gap after each letter (except last letter of last group)
|
||||
if [ $i -lt $((${#group} - 1)) ]; then
|
||||
echo "file '${TMPDIR}/gap_letter.wav'" >> "$CONCAT_LIST"
|
||||
fi
|
||||
done
|
||||
# 1.0s gap after each group
|
||||
echo "file '${TMPDIR}/gap_group.wav'" >> "$CONCAT_LIST"
|
||||
done
|
||||
|
||||
# Concatenate all letter parts into one track
|
||||
ffmpeg -f concat -safe 0 -i "$CONCAT_LIST" \
|
||||
-c:a pcm_s16le "${TMPDIR}/letters.wav" -y 2>/dev/null
|
||||
|
||||
# --- 3. Generate closing announcement ---
|
||||
OUTRO_TEXT="END TRANSMISSION. TUNE TO OLD COMPUTER NERD DOT COM FOR FURTHER INSTRUCTIONS."
|
||||
echo "$OUTRO_TEXT" | \
|
||||
espeak-ng -ven+f3 -s88 -k50 -a 200 -w "${TMPDIR}/outro.wav" --punct="" 2>/dev/null
|
||||
|
||||
# --- 4. Concatenate intro + letters + outro ---
|
||||
VOICE_LIST="${TMPDIR}/voice_concat.txt"
|
||||
echo "file '${TMPDIR}/intro.wav'" > "$VOICE_LIST"
|
||||
echo "file '${TMPDIR}/gap_group.wav'" >> "$VOICE_LIST"
|
||||
echo "file '${TMPDIR}/letters.wav'" >> "$VOICE_LIST"
|
||||
echo "file '${TMPDIR}/gap_group.wav'" >> "$VOICE_LIST"
|
||||
echo "file '${TMPDIR}/outro.wav'" >> "$VOICE_LIST"
|
||||
|
||||
ffmpeg -f concat -safe 0 -i "$VOICE_LIST" \
|
||||
-ar ${SAMPLE_RATE} -ac 1 -c:a pcm_s16le "${TMPDIR}/voice.wav" -y 2>/dev/null
|
||||
|
||||
# --- 5. Concatenate poacher theme + voice into foreground track ---
|
||||
FG_LIST="${TMPDIR}/fg_concat.txt"
|
||||
echo "file '${TMPDIR}/poacher_resampled.wav'" > "$FG_LIST"
|
||||
echo "file '${TMPDIR}/voice.wav'" >> "$FG_LIST"
|
||||
|
||||
# Resample poacher intro to match
|
||||
ffmpeg -i "${ASSETS_DIR}/poacher_intro.wav" \
|
||||
-ar ${SAMPLE_RATE} -ac 1 -c:a pcm_s16le "${TMPDIR}/poacher_resampled.wav" -y 2>/dev/null
|
||||
|
||||
ffmpeg -f concat -safe 0 -i "$FG_LIST" \
|
||||
-c:a pcm_s16le "${TMPDIR}/foreground.wav" -y 2>/dev/null
|
||||
|
||||
# --- 6. Get foreground duration, generate static to match ---
|
||||
FG_DUR=$(ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${TMPDIR}/foreground.wav")
|
||||
echo " Audio duration: ${FG_DUR}s"
|
||||
|
||||
ffmpeg -f lavfi -i "anoisesrc=d=${FG_DUR}:c=0.22" \
|
||||
-af "highpass=f=2500,lowpass=f=9000,volume=0.18" \
|
||||
-ar ${SAMPLE_RATE} -ac 1 -t "${FG_DUR}" \
|
||||
-c:a pcm_s16le "${TMPDIR}/static.wav" -y 2>/dev/null
|
||||
|
||||
# --- 7. Mix static underneath foreground ---
|
||||
ffmpeg -i "${TMPDIR}/foreground.wav" -i "${TMPDIR}/static.wav" \
|
||||
-filter_complex "[0:a][1:a]amix=inputs=2:duration=longest:normalize=0" \
|
||||
-c:a pcm_s16le "${TMPDIR}/final_audio.wav" -y 2>/dev/null
|
||||
|
||||
# --- 8. Build video to match audio duration ---
|
||||
# Round up to nearest second for video duration
|
||||
VIDEO_DUR=$(echo "$FG_DUR" | awk '{printf "%d", $1 + 1}')
|
||||
|
||||
ffmpeg -f lavfi -i "color=black:s=1280x720:d=${VIDEO_DUR}" \
|
||||
-i "${TMPDIR}/final_audio.wav" \
|
||||
-vf "drawtext=font='Courier':fontsize=50:fontcolor=green@0.95:text='NERDLETTER SECRET TRANSMISSION #${EPISODE}':x=(w-text_w)/2:y=80, \
|
||||
drawtext=font='Courier':fontsize=36:fontcolor=green@0.85:text='USE DAY BOOK PAGE ${PAGE}':x=(w-text_w)/2:y=170, \
|
||||
drawtext=font='Courier':fontsize=44:fontcolor=lime@0.95:text='${MESSAGE}':x=(w-text_w)/2:y=270:box=1:boxcolor=black@0.75:boxborderw=15, \
|
||||
drawtext=font='Courier':fontsize=30:fontcolor=green@0.75:text='DESTROY PAGE AFTER USE — SUBSCRIBERS ONLY':x=(w-text_w)/2:y=640" \
|
||||
-c:v libx264 -pix_fmt yuv420p -c:a aac -shortest "$OUTFILE" -y
|
||||
|
||||
echo "Done: ${OUTFILE}"
|
||||
15
pyproject.toml
Normal file
15
pyproject.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[project]
|
||||
name = "nerdletter"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "Gregory Gauthier",email = "gregory.gauthier@perspectum.com"}
|
||||
]
|
||||
requires-python = "^3.14"
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
4
site/deploy.sh
Executable file
4
site/deploy.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
scp index.html gmgauthier@socrates:/var/www/nerdletter.oldcomputernerd.com
|
||||
|
||||
124
site/index.html
Normal file
124
site/index.html
Normal file
@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Nerdletter • Retro-Modern Fusion</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); /* Perfect CRT font */
|
||||
body {
|
||||
background: #000;
|
||||
color: #00ff41;
|
||||
font-family: 'VT323', monospace;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
image-rendering: pixelated;
|
||||
text-shadow: 0 0 8px #00ff41;
|
||||
}
|
||||
.crt {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
border: 16px solid #222;
|
||||
background: #001100;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 0 40px #00ff41, inset 0 0 80px rgba(0,255,65,0.2);
|
||||
}
|
||||
h1 {
|
||||
font-size: 3.5rem;
|
||||
text-align: center;
|
||||
margin: 0 0 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
animation: flicker 1.5s infinite alternate;
|
||||
}
|
||||
@keyframes flicker { 0% { opacity: 0.95; } 100% { opacity: 1; } }
|
||||
.tagline { text-align: center; font-size: 1.8rem; margin-bottom: 2rem; color: #00cc33; }
|
||||
.scanlines {
|
||||
position: relative;
|
||||
}
|
||||
.scanlines::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: repeating-linear-gradient(transparent 0px, transparent 2px, rgba(0,255,65,0.07) 2px, rgba(0,255,65,0.07) 4px);
|
||||
pointer-events: none;
|
||||
}
|
||||
section { margin: 3rem 0; }
|
||||
h2 { color: #00ff9d; border-bottom: 3px double #00ff41; padding-bottom: 0.5rem; }
|
||||
.issue-teaser {
|
||||
background: #001a00;
|
||||
padding: 1.5rem;
|
||||
border: 4px solid #00ff41;
|
||||
}
|
||||
.subscribe {
|
||||
text-align: center;
|
||||
background: #003300;
|
||||
padding: 2rem;
|
||||
border: 6px double #00ff41;
|
||||
}
|
||||
a { color: #00ff9d; text-decoration: underline dotted; }
|
||||
a:hover { color: #ffff00; }
|
||||
footer { text-align: center; font-size: 1.1rem; margin-top: 4rem; opacity: 0.7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="crt scanlines">
|
||||
<h1>THE NERDLETTER</h1>
|
||||
<p class="tagline">COMPUTING WHEN IT WAS STILL FUN</p>
|
||||
|
||||
<section>
|
||||
<h2>INCOMING TRANSMISSION</h2>
|
||||
<div class="issue-teaser">
|
||||
<video controls width="100%" style="margin-bottom: 1rem; border: 2px solid #00ff41;">
|
||||
<source src="https://gmgauthier.us-east-1.linodeobjects.com/videos/nerdletter-secret-01.mp4" type="video/mp4">
|
||||
</video>
|
||||
If you arrived here by way of a secret transmission, consult your day book
|
||||
for decyphering instructions. Starting Monday, June 1, there will be a new secret message each week.<br><br>
|
||||
No day book? You may obtain one by emailing
|
||||
<a href="mailto:nerdletter@gmgauthier.com">nerdletter@gmgauthier.com</a>
|
||||
with proof of a $5 PayPal subscription payment. You'll also receive a BASIC program that will help you
|
||||
decode the secret messages – compatible with almost all 8-bit era computers!
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>ISSUE #001 COMING SOON</h2>
|
||||
<div class="issue-teaser">
|
||||
<strong>Featured in our first issue:</strong><br>
|
||||
• News and Interviews with the greats of the past!<br>
|
||||
• DIY projects from community members!<br>
|
||||
• Modern Projects that are keeping your retro system alive!<br>
|
||||
• Guest essays from people who know what they’re talking about!<br>
|
||||
• Bonus content: Nerdy stories!
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Why a print newsletter in 2026?</h2>
|
||||
<p>We all remember the days when you had to send away for stuff. The self-addressed stamped envelope.
|
||||
The $5 check. The anxious checking of the post box for your magazine. We also remember the joy of
|
||||
having that print in our hands. The feeling of opening it up and seeing fresh content inside, and
|
||||
the eager anticipation of that moment when you'd have time to sit down with a cup of coffee and
|
||||
read. That's why this exists. Because some things are better with ink and paper. No pop-ups.
|
||||
No tracking. Just you, a cup of coffee, and stories about the machines that shaped us — plus the
|
||||
crazy modern hacks that keep them alive.</p>
|
||||
<p>Small print run. Big passion. For retro-fusion lovers only.</p>
|
||||
</section>
|
||||
|
||||
<div class="subscribe">
|
||||
<h2>GET YOUR COPY</h2>
|
||||
<p>Dot-matrix printed edition mailed directly to your door.<br>
|
||||
Limited run — When the paper runs out, that's it.</p>
|
||||
<p><strong>Drop me a line:</strong> <a href="mailto:nerdletter@gmgauthier.com">nerdletter@gmgauthier.com</a></p>
|
||||
<p>I’ll send you the details.</p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
© 2026 The Nerdletter • Old Computer Nerd HQ • Oxford, UK<br>
|
||||
<span style="font-size:0.9rem;">"If it booted in 1985, it'll boot in 2026."</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user