A local journal app themed after Serial Experiments Lain's recurring Layer:0X title cards.
Each entry is a Layer. Give it a title and body, click it, and the title-card animation plays: glowing liquid-glass letterforms scatter across the screen and strobe-flicker into place while a distorted voice builds up and resolves cleanly into "Layer zero one." The reverb and echo values were derived from autocorrelation analysis of the actual source audio.
- Python 3 — stdlib only, no pip install needed
espeak-ngfor speech synthesis
Clone the repo and install espeak-ng for your distro, then run the server:
git clone https://github.com/Ymsniper/lain-layers.git
cd lain-layers
python3 server.pyThen open http://localhost:8765 in your browser.
| Distro | Command |
|---|---|
| Arch / CachyOS / Manjaro | sudo pacman -S espeak-ng |
| Debian / Ubuntu / Mint / Pop!_OS | sudo apt install espeak-ng |
| Fedora / RHEL 8+ / CentOS Stream | sudo dnf install espeak-ng |
| openSUSE Tumbleweed / Leap | sudo zypper install espeak-ng |
| Void Linux | sudo xbps-install espeak-ng |
| Alpine Linux | sudo apk add espeak-ng |
| Gentoo | sudo emerge app-accessibility/espeak-ng |
| NixOS | nix-env -iA nixpkgs.espeak-ng |
| Solus | sudo eopkg install espeak-ng |
| Slackware | slackpkg install espeak-ng |
Entries are saved to entries.json next to server.py. Nothing leaves your machine.
The show uses Apple's proprietary PlainTalk "Whisper" voice — unavailable on Linux. This app uses espeak-ng's built-in whisper variant (espeak-ng -v en+whisper), the closest free offline equivalent.
The in-app ⚙ voice panel (bottom-right) lets you tune variant, rate, pitch, reverb, echo, and playback speed. Settings persist via localStorage.
Speech is processed through a pure-Python DSP chain (wave + array, no dependencies):
| Layer | Description |
|---|---|
| Tight reverb | 4 early reflections at 29 / 42 / 67 / 95 ms — from raw-waveform autocorrelation of the source audio |
| Diffuse wash | 13 overlapping taps from 120 ms – 850 ms |
| Discrete echo | 323 ms slap-back — located via envelope cross-correlation of the loudest voice burst |
Each clip uses a per-sample wet/dry envelope so distortion evolves across the clip's own duration rather than sitting at a fixed level. The server returns the exact rendered clip length in X-Audio-Duration so the frontend animation stays in sync without guessing.
Each title-word letter is independently animated:
- Position — scatter → flicker between waypoints via
steps(1)→ smooth bezier settle - Opacity — strobe blink (5× snap-on/off) while position is live, then hold at 1
- Ghosts — 3 tight afterimage copies per letter strobe at offset positions, vanishing as the real letter arrives
- Echo — large faint rotated duplicate of the settled word fades in with the caption
- Layout — even/odd layer numbers alternate which corner the word and caption occupy
Animation duration is derived from buildup.duration / speed, so audio and visual are always in sync by construction.
lain-layers/
├── server.py # stdlib HTTP server: entries CRUD + /api/speak (espeak-ng + DSP)
├── index.html # page structure
├── style.css # theme + title-card keyframe animation
├── app.js # app logic + letter-assembly animation
├── entries.json # local journal data (auto-created, git-ignored)
└── assets/
├── Lovelt__.ttf # display font (Love Letter TW)
└── titlecard-bg.jpg # title card background
MIT © 2026 Ymsniper
