A self-hosted Rust web application for learning Korean Hangul using spaced repetition with the modern FSRS algorithm. Multi-user support with per-user databases.
- Multi-User Support: User registration/login with isolated per-user databases
- FSRS Algorithm: Modern Free Spaced Repetition Scheduler (20-30% more efficient than SM-2)
- Interactive Learning: Type romanization or select Korean from multiple choice - no passive reveal-and-rate
- Progressive Hints: 3-level hint system (length → description → partial reveal)
- Confusion Tracking: Identifies problem characters and common mistakes
- Tiered Learning: Progress from basic to advanced characters
- Tier 1: Basic consonants (ㄱ, ㄴ, ㄷ...) and vowels (ㅏ, ㅓ, ㅗ...)
- Tier 2: Y-vowels (ㅑ, ㅕ...) and special ieung (ㅇ)
- Tier 3: Aspirated (ㅋ, ㅍ...) and tense consonants (ㄲ, ㅃ...)
- Tier 4: Compound vowels (ㅘ, ㅝ...)
- Accelerated Mode: Unlock all tiers immediately for experienced learners
- Focus Mode: Study specific tiers only
- Listening Practice: Audio recognition with syllable playback
- Practice Mode: Untracked learning without affecting SRS
- Self-Hosted: No cloud dependencies, runs on your own hardware
- Mobile-Responsive: Hamburger menu, touch-friendly buttons, double-tap submit
- Haetae Mascot: Animated Korean guardian companion
- Backend: Axum (async web framework)
- Database: SQLite via rusqlite
- Templating: Askama (compile-time templates)
- Frontend: HTMX 2.x + Tailwind CSS (build-time)
- SRS: FSRS 5.2 (with SM-2 fallback)
The easiest way to run the app, especially for LAN hosting:
git clone https://github.com/PheelaV/kr_notebook.git
cd kr_notebook
docker compose up -dAccess at http://localhost:3000 or http://<your-ip>:3000 on your LAN.
# View logs
docker compose logs -f
# Stop
docker compose downUser data persists in ./data/.
Run the app on a home server and access it securely from anywhere using Tailscale:
-
Install Tailscale on your server and devices:
# On your server (Linux) curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up
-
Run the app (Docker or native):
docker compose up -d
-
Access from any device on your Tailnet:
http://<server-tailscale-ip>:3000Find your server's Tailscale IP with
tailscale ipor in the Tailscale admin console. -
Optional: Use MagicDNS for a friendly hostname:
http://<server-hostname>:3000Enable MagicDNS in Tailscale admin → DNS settings.
Benefits:
- No port forwarding or exposing to the internet
- Encrypted connections between devices
- Access from mobile (install Tailscale app)
- Works behind NAT/firewalls
- Optional: Tailscale Funnel for public access (share with friends not on your Tailnet):
This gives you a public
# Enable HTTPS funnel on port 3000 sudo tailscale funnel 3000https://<hostname>.<tailnet>.ts.netURL. Enable Funnel in Tailscale admin → Access controls first.
- Rust 1.80+ (edition 2024)
- Tailwind CSS v4 standalone CLI (for development builds)
- Download from: https://github.com/tailwindlabs/tailwindcss/releases
- Place in PATH (e.g.,
~/.local/bin/tailwindcss)
git clone https://github.com/PheelaV/kr_notebook.git
cd kr_notebook
cargo runThe server starts at http://localhost:3000
| Tool | Version | Purpose | Install |
|---|---|---|---|
| Rust | 1.80+ | Backend, WASM | rustup.rs |
| Node.js | 18+ | E2E tests, JS unit tests | nodejs.org |
| Python | 3.12+ | Integration tests, tooling | System or pyenv |
| uv | latest | Python package manager | curl -LsSf https://astral.sh/uv/install.sh | sh |
| Tailwind CSS | v4 | CSS compilation | releases |
| wasm-pack | latest | WASM builds (optional) | cargo install wasm-pack |
| cross | latest | ARM64 builds (optional) | cargo install cross |
# Clone and build
git clone https://github.com/PheelaV/kr_notebook.git
cd kr_notebook
cargo build
# Install test dependencies
cd tests/e2e && npm install && npx playwright install && cd ../..
cd tests/js && npm install && cd ../..
cd tests/integration && uv sync && cd ../..
cd py_scripts && uv sync && cd ..# Hot reload
cargo install cargo-watch
cargo watch -x run
# Tests (all)
./scripts/test.sh
# Tests (by level)
./scripts/test.sh unit # Rust + JS + Python unit tests
./scripts/test.sh integration # + Python integration tests
./scripts/test.sh e2e # E2E only (Playwright)
./scripts/test.sh all # Everything
# Lint
cargo clippyFor deploying to Raspberry Pi or other ARM64 Linux targets:
# Install cross
cargo install cross
# Build for ARM64
cross build --release --target aarch64-unknown-linux-gnuThe included Cross.toml automatically installs Tailwind CLI in the build container.
Deployment scripts are available in scripts/:
rpi-setup.sh- Initial RPi configurationrpi-deploy.sh- Deploy binary and static assetssync-audio.sh- Sync pronunciation audio
Start at the home page (/) to see due cards and begin studying. Key pages:
| Route | Description |
|---|---|
/study |
Interactive study with typed/selected answers |
/practice |
Untracked practice mode |
/progress |
View learning progress and statistics |
/settings |
Configure tiers, algorithm, and preferences |
See doc/07_endpoints.md for complete API documentation (67 endpoints).
Configuration via config.toml (copy from config.toml.example):
cp config.toml.example config.tomldata/
├── app.db # Shared database (users, sessions, card definitions)
├── content/
│ └── packs/ # Content pack definitions
│ ├── baseline/ # Built-in Hangul characters
│ └── htsk-scraper/ # HTSK audio generator pack
└── users/<username>/
└── learning.db # Per-user progress database
Each user gets an isolated database with their own SRS state, progress, and settings. Card definitions are shared in app.db, while user progress is stored per-user.
The app uses a simple username/password authentication system:
- Registration: Create account at
/register(username + password) - Login: Authenticate at
/login - Sessions: HTTP-only cookies, 7-day expiry
- Password Storage: Client-side SHA-256 → server-side Argon2 (server never sees plaintext)
All routes except /login and /register require authentication.
kr_notebook/
├── Cargo.toml
├── build.rs # Tailwind CSS build + asset hashing
├── askama.toml # Askama configuration
├── Dockerfile # Multi-stage Rust build
├── docker-compose.yml # LAN deployment (kr_notebook + py-tools)
├── src/ # Rust backend
│ ├── main.rs # Server entry point
│ ├── lib.rs # Module exports
│ ├── state.rs # AppState (shared auth DB, paths)
│ ├── paths.rs # Centralized path constants
│ ├── config.rs # Configuration loading
│ ├── audio.rs # Audio file handling
│ ├── session.rs # Session ID generation
│ ├── filters.rs # Template filters
│ ├── validation.rs # Answer validation
│ ├── testing.rs # Test utilities
│ ├── input.css # Tailwind input CSS
│ ├── auth/ # Authentication system
│ │ ├── mod.rs # Module exports
│ │ ├── db.rs # Auth database (users, sessions)
│ │ ├── handlers.rs # Login, register, logout
│ │ ├── middleware.rs # Auth middleware, AuthContext
│ │ └── password.rs # Argon2 hashing
│ ├── content/ # Content pack system
│ │ ├── mod.rs # Module exports
│ │ ├── cards.rs # Card loading from packs
│ │ ├── audio.rs # Audio file resolution
│ │ ├── packs.rs # Pack definitions
│ │ ├── discovery.rs # Pack discovery
│ │ └── generator.rs # Content generators
│ ├── db/ # User database layer
│ │ ├── mod.rs # Pool management, seed data
│ │ ├── schema.rs # Table definitions
│ │ ├── cards.rs # Card queries
│ │ ├── reviews.rs # Review operations
│ │ ├── stats.rs # Statistics
│ │ ├── tiers.rs # Tier progress
│ │ └── lesson_progress.rs # Lesson progress
│ ├── domain/ # Data models
│ ├── handlers/ # HTTP handlers
│ │ ├── mod.rs # Index, exports
│ │ ├── study/ # Study handlers
│ │ │ ├── mod.rs
│ │ │ ├── interactive.rs # Interactive study mode
│ │ │ ├── classic.rs # Classic reveal-and-rate
│ │ │ ├── practice.rs # Untracked practice
│ │ │ └── templates.rs # Shared templates
│ │ ├── settings/ # Settings handlers
│ │ │ ├── mod.rs
│ │ │ ├── user.rs # User settings, export/import
│ │ │ ├── admin.rs # Admin functions, tier management
│ │ │ └── audio.rs # Pronunciation audio config
│ │ ├── listen.rs # Listening practice
│ │ ├── progress.rs # Progress analytics
│ │ ├── pronunciation.rs # Audio matrix
│ │ ├── library.rs # Character browser
│ │ ├── reference.rs # Reference pages
│ │ ├── guide.rs # Usage guide
│ │ ├── diagnostic.rs # Diagnostic endpoints
│ │ └── vocabulary.rs # Vocabulary browser
│ ├── profiling/ # Optional (--features profiling)
│ ├── services/ # Business logic
│ │ └── pack_manager.rs # Pack management
│ └── srs/ # Spaced repetition (FSRS + SM-2)
├── py_scripts/ # Python tools
│ ├── Dockerfile # Python + ffmpeg image
│ ├── pyproject.toml
│ └── src/
│ ├── kr_scraper/ # Audio scraper
│ └── db_manager/ # Database scenarios CLI
├── templates/ # Askama HTML templates
├── doc/ # Documentation
└── data/ # Runtime data (gitignored)
├── app.db # Shared auth database
├── content/
│ └── packs/ # Content pack definitions
├── users/<username>/ # Per-user data
│ └── learning.db # User's learning database
└── scraped/htsk/ # Scraped audio + manifests
The app uses a modular content pack system to organize learning content:
- Baseline Pack: Built-in Hangul characters (consonants, vowels, compound vowels). Always enabled, cannot be disabled.
- Card Packs: Additional card sets that can be enabled/disabled per user.
- Audio Packs: Pronunciation audio for characters and syllables.
- Generator Packs: Scripts that create content (e.g., HTSK audio scraper).
View and manage content packs in Settings → Content Packs:
- See all available packs with descriptions
- Enable/disable packs (except baseline)
- Enabled packs add their cards to your study queue
Pack definitions are stored in data/content/packs/ with a pack.json manifest describing the pack type, content, and metadata.
New cards use a learning phase with short intervals before graduating to long-term FSRS scheduling:
| Step | Normal Mode | Focus Mode |
|---|---|---|
| 0 | 1 min | 1 min |
| 1 | 10 min | 5 min |
| 2 | 1 hour | 15 min |
| 3 | 4 hours | 30 min |
| 4+ | FSRS (~1+ day) | FSRS |
Cards must be answered correctly through all 4 learning steps to graduate. Incorrect answers reset to step 0.
Free Spaced Repetition Scheduler - tracks memory stability and difficulty per card:
- Rating: Again (1), Hard (2), Good (3), Easy (4)
- Retention target: 90% (configurable)
- More details: open-spaced-repetition/fsrs-rs
Classic SuperMemo 2 algorithm:
- Rating: Again (0), Hard (2), Good (4), Easy (5)
- Ease factor: Adjusts based on performance (min 1.3)
The app supports pronunciation audio from howtostudykorean.com (Lessons 1-2).
cd py_scripts
uv sync # Install dependencies
uv run kr-scraper lesson1 # Download Lesson 1 audio
uv run kr-scraper lesson2 # Download Lesson 2 audio
uv run kr-scraper segment # Segment into syllablesThe docker-compose.yml includes a py-tools service with Python 3.12 and ffmpeg pre-installed:
docker compose run --rm py-tools kr-scraper lesson1
docker compose run --rm py-tools kr-scraper lesson2
docker compose run --rm py-tools kr-scraper segment
docker compose run --rm py-tools kr-scraper statusThe data/ directory is shared between py-tools and the main kr_notebook service.
Requires Python 3.12+, uv, and ffmpeg (for audio segmentation):
# Install ffmpeg (Ubuntu/Debian)
sudo apt install ffmpeg
# Run scraper
cd py_scripts && uv sync
uv run kr-scraper lesson1
uv run kr-scraper lesson2
uv run kr-scraper segmentManifests (data/scraped/htsk/*/manifest.json) contain segmentation parameters
and are version-controlled. After cloning, regenerate audio using the saved parameters:
cd py_scripts
uv run kr-scraper lesson1 && uv run kr-scraper lesson2 && uv run kr-scraper segmentSettings → Pronunciation Audio → Preview allows adjusting parameters per row:
- s: Min silence (ms) - gap detection threshold
- t: Threshold (dBFS) - silence detection sensitivity
- P: Padding (ms) - buffer before/after segments
- skip: Skip first N segments (for noisy audio)
Manage database scenarios for testing:
uv run db-manager status # Show current database
uv run db-manager list # List scenarios
uv run db-manager use <name> # Switch scenario
uv run db-manager create <name> # Create from current
uv run db-manager reset # Reset to goldenEnable profiling to log handler timing and DB queries:
cargo run --features profilingOutputs:
- Console:
[PROFILE] {...}JSON lines - File:
data/profile_{timestamp}.jsonl
doc/01_learning_fsa.md- Learning mode state machinedoc/02_card_selection.md- Card selection algorithmdoc/04_authentication.md- Authentication systemdoc/05_database.md- Database schema (app.db + learning.db)doc/07_endpoints.md- API endpoint referencedoc/08_testing.md- Testing guide
Pronunciation audio is sourced from How to Study Korean, an excellent free resource for learning Korean:
- Unit 0 Lesson 1 - Basic consonants (ㅂㅈㄷㄱㅅㅁㄴㅎㄹ) and vowels (ㅣㅏㅓㅡㅜㅗ)
- Unit 0 Lesson 2 - Tense (ㄲㅃㅉㄸㅆ) and aspirated (ㅋㅍㅊㅌ) consonants
- Unit 0 Lesson 3 - Compound vowels (ㅐㅔㅒㅖㅚㅟㅢㅘㅙㅝㅞ)
Audio files are not redistributed with this project. Users must run the scraper to download audio for personal educational use.


