feat(project): add initial setup with IDE configs, gitignore, and final script
- Introduce .gitignore to exclude output directory - Add IntelliJ/PyCharm project files under .idea/ - Create CLAUDE.md for Claude Code guidance on the project - Add scripts/nerdletter_numbers_station_final.sh as the refined Lincolnshire Poacher style generator - Update sample output MP4 in output/
This commit is contained in:
parent
400b41b802
commit
5cd09628ee
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
output/
|
||||
10
.idea/.gitignore
vendored
Normal file
10
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/nerdletter-cypher.iml" filepath="$PROJECT_DIR$/.idea/nerdletter-cypher.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/nerdletter-cypher.iml
Normal file
8
.idea/nerdletter-cypher.iml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
45
CLAUDE.md
Normal file
45
CLAUDE.md
Normal file
@ -0,0 +1,45 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Nerdletter Cypher is a collection of bash scripts that generate "numbers station" style MP4 videos for the Nerdletter newsletter. Each video displays a ciphertext message with retro green-on-black terminal aesthetics and a synthesized voice reading the message aloud, mimicking cold-war shortwave numbers stations (specifically the Lincolnshire Poacher style).
|
||||
|
||||
Subscribers use a "Day Book" (a one-time pad or codebook) to decode the ciphertext messages.
|
||||
|
||||
## Script Variants
|
||||
|
||||
All scripts live in `scripts/` and share a common interface:
|
||||
|
||||
```bash
|
||||
./script.sh "CIPHERTEXT GROUPS" PAGE_NUMBER [EPISODE_NUMBER]
|
||||
# Example: ./script.sh "HLXIB JACJP ERXHN EFM" 001 01
|
||||
```
|
||||
|
||||
- **nerdletter_numbers_station.sh** — Original basic version (espeak-ng + ffmpeg, single pass)
|
||||
- **nerdletter_numbers_station_linux.sh** — Linux variant with letter-by-letter pronunciation
|
||||
- **nerdletter_numbers_station_mac.sh** / **_mac_old.sh** — macOS variants (uses `Courier New` font for Homebrew ffmpeg compatibility)
|
||||
- **nerdletter_numbers_station_poacher.sh** through **poacher10.sh** — Iterative refinements adding: Lincolnshire Poacher intro music, shortwave static mixing, letter-by-letter speech with inter-group pauses, longer durations, and audio layering
|
||||
|
||||
The `poacher` series represents progressive iterations. The highest-numbered poacher script (`poacher10.sh`) is the most refined version with letter-by-letter speech, pauses between cipher groups, and full audio mixing (poacher intro + static + voice).
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **espeak-ng** — text-to-speech synthesis (voice: `en+f3`, female)
|
||||
- **ffmpeg** — video generation, audio mixing, and final MP4 encoding
|
||||
- **wget** — (poacher scripts only) for downloading the Lincolnshire Poacher intro audio from archive.org
|
||||
|
||||
## Assets
|
||||
|
||||
- `assets/poacher_intro.wav` — Lincolnshire Poacher music intro clip
|
||||
- `assets/static.wav` — Shortwave radio static noise
|
||||
- `output/` — Generated MP4 files land here (though some scripts output to the current working directory)
|
||||
|
||||
## Key Parameters Across Scripts
|
||||
|
||||
espeak-ng flags control the voice character:
|
||||
- `-s` speech rate (lower = slower/more dramatic, ranges from 74-120 across variants)
|
||||
- `-k` capitals emphasis
|
||||
- `-a` amplitude/volume
|
||||
- `-ven+f3` English female voice variant 3
|
||||
Binary file not shown.
122
scripts/nerdletter_numbers_station_final.sh
Executable file
122
scripts/nerdletter_numbers_station_final.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}"
|
||||
Loading…
Reference in New Issue
Block a user