commit 33c9b162674b0aef57abbee189137fcc9aae77c3 Author: Gregory Gauthier Date: Tue Apr 28 13:20:50 2026 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34e6020 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/cipher/__init__.py b/cipher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cipher/assets/poacher_intro.wav b/cipher/assets/poacher_intro.wav new file mode 100644 index 0000000..242bc15 Binary files /dev/null and b/cipher/assets/poacher_intro.wav differ diff --git a/cipher/assets/static.wav b/cipher/assets/static.wav new file mode 100644 index 0000000..5036293 Binary files /dev/null and b/cipher/assets/static.wav differ diff --git a/cipher/data/daybook_master_example.txt b/cipher/data/daybook_master_example.txt new file mode 100644 index 0000000..48c772b --- /dev/null +++ b/cipher/data/daybook_master_example.txt @@ -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 === \ No newline at end of file diff --git a/cipher/encrypt.py b/cipher/encrypt.py new file mode 100644 index 0000000..0e47fb4 --- /dev/null +++ b/cipher/encrypt.py @@ -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.") \ No newline at end of file diff --git a/cipher/instructions.md b/cipher/instructions.md new file mode 100644 index 0000000..b61a07f --- /dev/null +++ b/cipher/instructions.md @@ -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. diff --git a/cipher/output/.placeholder b/cipher/output/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/cipher/scripts/nerdletter_numbers_station.sh b/cipher/scripts/nerdletter_numbers_station.sh new file mode 100755 index 0000000..778065b --- /dev/null +++ b/cipher/scripts/nerdletter_numbers_station.sh @@ -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}" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..71b9950 --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/site/deploy.sh b/site/deploy.sh new file mode 100755 index 0000000..6c16b99 --- /dev/null +++ b/site/deploy.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env zsh + +scp index.html gmgauthier@socrates:/var/www/nerdletter.oldcomputernerd.com + diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..5cac401 --- /dev/null +++ b/site/index.html @@ -0,0 +1,124 @@ + + + + + + The Nerdletter • Retro-Modern Fusion + + + +
+

THE NERDLETTER

+

COMPUTING WHEN IT WAS STILL FUN

+ +
+

INCOMING TRANSMISSION

+
+ + 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.

+ No day book? You may obtain one by emailing + nerdletter@gmgauthier.com + 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! +
+
+ +
+

ISSUE #001 COMING SOON

+
+ Featured in our first issue:
+ • News and Interviews with the greats of the past!
+ • DIY projects from community members!
+ • Modern Projects that are keeping your retro system alive!
+ • Guest essays from people who know what they’re talking about!
+ • Bonus content: Nerdy stories! +
+
+ +
+

Why a print newsletter in 2026?

+

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.

+

Small print run. Big passion. For retro-fusion lovers only.

+
+ + + + +
+ +