Active · May 23, 2026
TubeShadow
A YouTube-based English shadowing tool with clip mining, AI-explained transcripts, and SM-2 spaced repetition. Built for developers and knowledge workers.
- Role
- Solo
- Stack
- Java 21 · Spring Boot 3.3 · PostgreSQL · Next.js 16 · TypeScript · Tailwind 4 · Claude Haiku
A YouTube clip miner + shadowing trainer. Paste a YouTube URL, pull subtitles and metadata, cut free clips with start/end markers or double-clicking captions, then put them into a personal library with tags, decks, and SM-2 spaced repetition.
What it does
- Import: YouTube URL →
yt-dlpextracts subtitles and metadata. Idempotent self-healing import (re-running fixes transient transcript failures). - Clip: subtitle double-click or manual start/end. Loop with 0.5–1.5x playback speed.
- Library: search by name or subtitle text, filter by tag, sort, JSON export.
- AI analysis: per-clip Claude Haiku call (context, grammar, expressions, vocab) cached in Postgres JSONB — one LLM call per clip forever.
- Record + A/B: MediaRecorder → upload → original vs your take playback, accumulated over time.
- Review: SM-2 algorithm (Anki-style Again/Hard/Good/Easy), streak widget, deck filter.
- Curated content: shipped with a starter video collection (
curated-videos.yml) so new users have something to do day one. - Five languages: next-intl path-based routing (en default + ko / ja / zh / es).
Stack
Backend: Java 21 · Spring Boot 3.3 · Gradle Kotlin DSL · PostgreSQL · Flyway · Spring Security + JWT. Frontend: Next.js 16 (App Router) · TypeScript strict · Tailwind 4 · shadcn/ui · Zustand · TanStack Query. AI: Anthropic Claude Haiku 4.5 with prompt caching (also pluggable Gemini 2.5 Flash for free-tier deployments).
What it doesn't do (yet)
- No deployment URL yet — runs locally via
docker-compose up. - BYOAI (Bring Your Own AI) for users without API keys is partially shipped — Claude / Gemini / Perplexity prompts that you paste into your own chat tool.
- No mobile app. Browser only, mobile-first responsive.
Project log
Chronological record of troubleshooting, retros, and updates while building this.
AuthRateLimitFilter: Filter auto-registration trap → HandlerInterceptor
Tech retroMay 23, 2026 · 1 min
A `Filter` bean was auto-registered by Spring Boot independent of the `SecurityFilterChain` wiring and tried to instantiate it with a default constructor it didn't have. Switched to `HandlerInterceptor`; same behavior, no auto-registration trap.
YouTube transcript fetch replaced with yt-dlp + self-healing import
TroubleshootMay 23, 2026 · 1 min
The `timedtext` URL embedded in YouTube watch HTML carries a short-lived token; subtitle fetch returned 200 with empty body within minutes. Migrated entirely to `yt-dlp` subprocess and added an idempotent recovery hook so re-importing the same URL retries the transcript and the dimension probe.
BYOAI — send analysis prompts to the user's own ChatGPT/Claude/Gemini
MonetizationMay 24, 2026 · 1 min
Built a 'Send to my own AI' button that constructs an analysis prompt locally and opens ChatGPT/Claude/Gemini/Perplexity with `?q=...`, falling back to clipboard if auto-fill fails. Operating cost: zero; user keeps the conversation in their preferred model. Sets up a clean alternative to a paid tier.
StaleObjectStateException on clip delete cascading to recordings
TroubleshootMay 24, 2026 · 1 min
Spring Data's derived `deleteByClipId` hydrates the entity into the persistence context before deleting it, racing with the DB-level `ON DELETE CASCADE`. Switched to `@Modifying @Query` so the delete bypasses the session cache.
i18n: next-intl with path-based routing across 5 locales
Tech retroMay 25, 2026 · 2 min
Migrated the entire frontend to `next-intl` with path-based routing (`/en`, `/ko`, `/ja`, `/zh`, `/es`), default English. All 17 user-facing files were keyed; `<Link>` and `useRouter` imports across the app moved to `@/i18n/navigation` wrappers. ja/zh/es fall back to English copy for now (translation is a non-engineering task).
Recording upload 415: MIME `Content-Type` whitelist must strip codec parameter
TroubleshootMay 26, 2026 · 1 min
Chrome's MediaRecorder tags audio with `audio/webm;codecs=opus`. The recording upload service compared the full string against a whitelist of base types, so Chrome uploads were rejected with 415. Stripped MIME parameters before the check.
AI provider abstraction — Claude credit ran out, swapped to Gemini in one env var
Tech retroMay 27, 2026 · 1 min
Anthropic credit hit zero mid-session. Introduced `AiAnalysisClient` interface; `ClaudeClient` and a new `GeminiClient` both implement it, each gated by `@ConditionalOnProperty(name = "tubeshadow.ai.provider")`. Switching providers is now one env var. Gemini's free tier (1500 req/day) covers personal use indefinitely; operating cost dropped to $0/mo.
Decks (Anki-style clip grouping) + Review next-due toast
UpdateMay 27, 2026 · 2 min
Two user-perception fixes shipped together. (1) Anki-style decks: a new `decks` table, nullable `deck_id` on `clips` (`ON DELETE SET NULL` so clips fall back to Inbox), library sidebar, per-card move dropdown, deck filter on review. (2) After Easy/Good/Hard/Again, a toast surfaces the actual next-due date so the clip doesn't feel like it 'vanished' from the queue.
Gemini 2.5 Flash: thinking tokens silently truncated JSON output
TroubleshootMay 27, 2026 · 1 min
After switching to Gemini, every analysis came back as `GEMINI_PARSE_FAILED: Unexpected end-of-input` at the same column. Cause: Gemini 2.5 Flash burns 'thinking' tokens before emitting visible output, eating most of `maxOutputTokens: 800` and truncating the JSON mid-string. Fix: `thinkingConfig: { thinkingBudget: 0 }` and bumped output cap to 4096.
Prompt-engineering 직독직해: forcing English word order in Korean output
UX retroMay 27, 2026 · 3 min
Asking Gemini for 'Korean chunks paired with English chunks' produced grammatical Korean in natural Korean order — useless for shadowing practice. Three prompt revisions: (1) hard rules + bad examples to force source order, (2) tune chunk size from 1-word to 2–5-word sense groups, (3) explicit BAD examples that show the failure mode.