This is a Next.js project bootstrapped with create-next-app.
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun devOpen http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.
This project uses next/font to automatically optimize and load Geist, a new font family for Vercel.
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.
Learn languages through the music you already love. LyricLang is a Duolingo-style language learning app that syncs real song lyrics with AI-generated exercises — so you practice vocabulary and grammar in the context of music you actually enjoy.
- Plays real YouTube music videos with synced lyrics
- Pauses at each lyric line and generates a language exercise
- Four exercise types: word match, sentence translation, fill-in-the-blank, and listening comprehension
- Every 8th line triggers a Lightning Round — a fast-fire timed quiz
- Tracks XP, level, streak, hearts, and a leaderboard
- Apple Music-style lyric display — active line large and bold, cascade fades for upcoming lines
- Desktop split-screen (player left, lyrics right) and mobile pill layout
Supported songs and languages:
| Song | Artist | Language |
|---|---|---|
| Nuevayol | Bad Bunny | Spanish |
| Hips Don't Lie | Shakira | Spanish |
| Alors on Danse | Stromae | French |
| Tout Oublier | Angele | French |
| Roar | Katy Perry | English |
| Firework | Katy Perry | English |
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript |
| Styling | Tailwind CSS v4 |
| Animation | Framer Motion v12 |
| State | Zustand v5 |
| AI | Anthropic Claude (Haiku) |
| Lyrics | lrclib.net API |
| Video | YouTube IFrame API via react-youtube |
git clone https://github.com/VMotta1/Lyric2Lang.git
cd Lyric2Lang
npm installCreate a .env.local file in the project root:
ANTHROPIC_API_KEY=sk-ant-...your key here...
Get a key at console.anthropic.com. Without it the app falls back to minimal placeholder exercises.
npm run devOpen http://localhost:3000.
lyric2lang/
├── app/
│ ├── (auth)/login/ # Login page
│ ├── (main)/
│ │ ├── page.tsx # Dashboard
│ │ ├── library/ # Song library
│ │ ├── leaderboard/ # Leaderboard
│ │ ├── profile/ # Profile page
│ │ └── learn/[songId]/
│ │ ├── page.tsx # Song overview
│ │ └── play/page.tsx # Play screen (main game)
│ ├── api/exercise/ # POST endpoint — calls Claude to generate exercises
│ └── globals.css # Tailwind v4 theme tokens + keyframes
│
├── components/
│ ├── exercises/
│ │ ├── FillInBlank.tsx
│ │ ├── LightningRound.tsx
│ │ ├── ListeningExercise.tsx
│ │ ├── TranslateSentence.tsx
│ │ └── WordMatch.tsx
│ ├── layout/
│ │ ├── BottomNav.tsx
│ │ └── Navbar.tsx
│ ├── player/
│ │ ├── ExerciseOverlay.tsx # Slide-up exercise container
│ │ ├── LyricDisplay.tsx # Apple Music-style lyric cascade
│ │ └── YouTubePlayer.tsx # YouTube iframe + polling loop
│ └── ui/ # XPBar, HeartCounter, StreakBadge, etc.
│
├── hooks/
│ └── useGameEngine.ts # State machine: playing → exercise → feedback → complete
│
├── lib/
│ ├── claude.ts # Exercise generation + caching + fallback
│ ├── claude-client.ts # Browser-safe wrapper (calls /api/exercise)
│ ├── claude-prompts.ts # Per-type prompt templates with song language awareness
│ ├── lrc.ts # lrclib.net fetch + LRC parser + lyric line builder
│ ├── store.ts # Zustand store (user, XP, hearts, leaderboard)
│ └── xp.ts # XP calculation and level math
│
├── data/
│ ├── songs.ts # Song definitions (6 demoable + 6 locked)
│ └── fallback-exercises.ts # Hardcoded exercises used when Claude API is unavailable
│
└── types/
└── index.ts # All shared TypeScript interfaces
- Lyrics load —
lrclib.netfetches timestamped lyrics. Falls back to hardcoded timestamps if the API has no data. - Video plays — YouTubePlayer runs a 200ms polling loop watching
getCurrentTime(). - Line triggers — When playback reaches the midpoint of a lyric line, the video pauses and an exercise loads.
- Exercise generates — Claude (Haiku) generates an exercise for that specific lyric line. The result is cached so it never regenerates for the same line in the same session.
- User answers — Correct = XP earned + combo builds. Wrong = lose a heart.
- Feedback shows — Green/red feedback for 1 second, then video resumes from where it paused.
- Session ends — After the last lyric line, a summary shows XP earned, accuracy, and lines practiced.
| Type | Description | Trigger |
|---|---|---|
| Word Match | Tap to match 4 song-language words to their translations | Lines 0, 4, 8… |
| Translate Sentence | Tap words to build the native-language translation | Lines 1, 5, 9… |
| Fill in the Blank | Song lyric with one word missing — pick from 4 options | Lines 2, 6, 10… |
| Listening | Hear the lyric, pick the correct translation | Lines 3, 7, 11… |
| Lightning Round | 30-second rapid-fire word pairs | Every 8th line |
All exercises are generated in the song's language. A Spanish song (Bad Bunny) produces Spanish exercises. A French song (Stromae) produces French exercises.
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
Yes | Anthropic API key for exercise generation |
The app is a standard Next.js project and deploys to any Node.js host.
Vercel (recommended):
npx vercelAdd ANTHROPIC_API_KEY as an environment variable in your Vercel project settings.
Single YouTubePlayer instance — The YouTube iframe must stay mounted to keep audio and the polling loop running. On mobile the player is positioned off-screen (fixed; top: -9999px) so it plays in the background while a visual thumbnail pill is shown instead.
Exercise caching — Both the server (lib/claude.ts) and browser (lib/claude-client.ts) cache exercises by ${songId}-${lineIndex}-${type}. Claude is never called twice for the same line in the same session.
Song language in prompts — Every Claude prompt explicitly states Song language: Spanish (or French/English) resolved from song.language in songs.ts. This prevents the model from mixing languages when the user's native language differs from the song language.
React 19 setState safety — useGameEngine maintains a stateRef mirror so submitAnswer can call updateXP/loseHeart (external Zustand updates) outside the React setState updater, complying with React 19's rule against side effects inside updaters.