유대선
Mimi

README

프로젝트 자체의 README를 여기 미러링했습니다. repo 상대 링크는 일반 텍스트입니다(repo가 비공개).

Mimi

Turn any YouTube video into spaced-repetition English shadowing — plus daily drills for the grammar patterns, collocations, and prepositions that never come automatically.

A full-stack English-learning app for developers who read English fluently but freeze when they have to produce it.

English · 한국어 README →

🌐 Frontend → mimi.daeseon.ai · 📓 Build log → daeseon.ai/projects/shadow-ai

Java 21 Spring Boot 3.3 Next.js 16 React 19 TypeScript PostgreSQL 16 CI License: MIT


TL;DR. A solo-built, full-stack English-learning app — Java/Spring Boot API + Next.js/TypeScript frontend. It has two halves that feed each other: (1) shadow from YouTube — clip a subtitle range and one LLM call returns a translation, a word-by-word literal gloss, vocabulary, per-preposition notes, and a practice scenario (cached in JSONB, so studying never re-bills the API), then SM-2 spaced repetition schedules it; (2) the Practice hub — daily Leitner-spaced drills over 82 sentence patterns, 101 word+preposition collocations, animated preposition diagrams, an AI composition check, and a weak-spots dashboard. The LLM provider (Gemini ↔ Claude) and recording storage (local FS ↔ S3/R2) flip by environment variable alone. Hardened from a prioritized codebase audit (7 batches), content accuracy-audited, and covered by Testcontainers + Vitest + Playwright.

Table of contents


What is Mimi?

Most developers read English docs all day and still freeze when they have to speak or write it. Mimi turns the input you already consume (YouTube) into the output practice you avoid — and drills the specific things that never come automatically (prepositions, fixed chunks, sentence frames) every day.

The thesis is friction removal, not feature count: input → output → repeat, the loop language-acquisition research actually supports. No pronunciation scoring, no AI essay grading, no social feed.


Why this project

For reviewers in a hurry — the engineering this repo demonstrates.


Product walkthrough

FlowWhat happens
Sign inEmail/password + JWT; per-user data from day one.
ImportYouTube URL → metadata (oEmbed) + subtitles (yt-dlp); re-import self-heals a missing transcript.
ClipRange by subtitle-click or start/end, auto transcript slice, loop + 0.5–1.5× speed, blind mode (full / first-letter / blocked), audio-only mode.
AI analysisOne LLM call → translation + word-by-word gloss (직독직해) + vocabulary + per-preposition notes + scenario, cached in JSONB.
ReviewSM-2 queue, three modes: reveal · write (L1 prompt → English, token-level diff) · scenario (AI situation → response).
Practice — Patterns82 sentence frames / 246 say-it-aloud items (the "as" family, relatives, indirect questions, conditionals, verb patterns…).
Practice — Collocations101 word+preposition chunks, General / Dev·IT filter.
Practice — PrepositionsAnimated per-sense SMIL diagrams + a fill-in drill + the prepositions mined from your own clips.
Practice — ComposeWrite your own sentence using a target → AI judges correctness, whether it used the target, and a better version.
Practice — Weak spotsHigh-lapse cards surfaced from the daily-grade data; seen / misses / mastered stats.
Drill engineLeitner SRS box per card (account-persisted), a shared daily streak, and the model sentence spoken aloud (Web Speech API).
Record + A/BRecord yourself, then play "original → you" back to back.
Decks & playlistAnki-style grouping (deleting a deck keeps the clips → Inbox); play a deck end-to-end with autoplay.
BYOAISend the analysis prompt to your own ChatGPT / Claude / Gemini — zero cost to the app.

Tech stack

LayerChoice
BackendJava 21 · Spring Boot 3.3.5 · Gradle (Kotlin DSL) · Spring Security + JWT (HS256) · Spring Data JPA
DatabasePostgreSQL 16 · Flyway (17 migrations) · raw-SQL CHECK constraints + JPA mapping
Async / resilience@TransactionalEventListener(AFTER_COMMIT) + @Async bounded pool · retry on 429/5xx/timeout
AIAiAnalysisClient interface → Gemini 2.5 Flash (default, free tier) or Claude Haiku 4.5, runtime-selected via @ConditionalOnProperty
Object storageLocal disk (dev) ↔ S3 / Cloudflare R2 (prod) — objects streamed through the backend (owner-gated), env-gated
ObservabilityMicrometer + Spring Actuator → Prometheus · MDC request/user logging
Mediayt-dlp subprocess (transcript + -J probe) · MediaRecorder capture with codec-param-tolerant MIME handling
FrontendNext.js 16 (App Router) · React 19 · TypeScript (strict) · Tailwind CSS v4 · shadcn/ui · Zustand · TanStack Query · next-intl
DrillsStatic typed content + Leitner SRS · Web Speech API (TTS) · animated SMIL preposition diagrams
Infra / deployDocker (multi-stage + ffmpeg/yt-dlp) · GitHub Actions CI (lint·test) + keyless OIDC deploy to AWS ECS Fargate + RDS · Vercel (frontend) · documented PaaS path
i18nen (default) · ko (full) · ja/zh/es scaffolded · cookie/path locale + SSR

Architecture

Feature-sliced backend, env-gated edges. Eight bounded contexts (auth, video, clip, analysis, recording, review, deck, practice) over a shared common module — organized by feature, not by layer. External capabilities are decided at a single seam, not sprinkled through the code.

Browser (Next.js, Vercel) ──JWT (Authorization)──▶ Spring Boot (8 bounded contexts)

   clip created ──▶ ApplicationEvent                   │
        │                                              ├─▶ PostgreSQL 16
   @TransactionalEventListener(AFTER_COMMIT) + @Async  │     users · videos · clips(transcript)
        │   (runs OUTSIDE the txn)                      │     clip_analyses (JSONB cache)
        ▼                                              │     review_items(SM-2) · decks · collections
   AiAnalysisClient ── ai.provider? ─▶ Gemini (free)   │     recordings · practice_progress(streak)
        │              └─ else ──────▶ Claude          │     practice_card (Leitner SRS)
        ▼   (1 call/clip → cached JSONB)               │
   ClipAnalysis: PENDING → READY / FAILED              ├─▶ Recording storage
                                                       │     RECORDING_STORAGE? ─▶ S3/R2 streamed (prod)
                                                       │                       └─▶ local disk (dev)
                                                       └─▶ Observability: MDC logs · Micrometer → Prometheus

Key decisions

Longer form: ARCHITECTURE.md.


Security & privacy

A prioritized codebase audit drove 7 hardening batches (safety/accuracy, quick wins, high-impact, observability, tests, architecture refactor, docs hygiene). Selected controls, each verified against source:

ConcernImplementation
AuthStateless JWT (HS256) + a token_version claim checked against the DB on every request → a password change revokes all older tokens.
Data isolationEvery user-scoped read is findByIdAndUserId(...) — another user's id returns 404, not someone else's row.
Media accessRecordings are never public. The route gates on a validated session (401) and ownership (403), then streams the bytes through the backend (InputStreamResource; local disk in dev, S3/R2 in prod) — never a public URL.
Upload MIMEMediaRecorder tags audio audio/webm;codecs=opus; the upload check compares the base type (parameters stripped) against an allow-list.
Rate limitingPer-IP on auth (fixed window); per-user on the AI composition endpoint — fixed-window interceptors returning 429 RATE_LIMITED.
PasswordsBCrypt-hashed (BCryptPasswordEncoder).
CORSCorsConfigurationSource scoped to the deployed frontend origins (mimi.daeseon.ai, Vercel previews).
Your dataExport saved clips as JSON; deleting a clip and its recording really removes the bytes (a @Modifying delete bypasses the session cache to avoid a StaleObjectStateException).

Data model & API

Eleven tables (backend/.../db/migration, bootstrapped by Flyway): users · videos · clips (transcript + note) · clip_analyses (JSONB analysis cache: translation, chunked 직독직해, vocabulary, preposition notes, scenario) · recordings · review_items (SM-2: easiness, interval, repetitions, due_date) · collections · collection_videos · decks · practice_progress (streak/reps, one per user) · practice_card (Leitner box per drill card).

Value constraints (analysis status, item type, …) are enforced by raw-SQL CHECK plus compile-time TypeScript/Java types.

API surface (backend/.../api, every response wrapped in ApiResponse<T>, the principal resolved via @CurrentUser):

Auth            POST /api/auth/{signup,login} · GET·PATCH /api/auth/me · POST /api/auth/me/password
Videos          POST /api/videos/import · GET /api/videos/{id}
Clips           POST·GET /api/clips · GET /api/clips/tags · GET·PATCH·DELETE /api/clips/{id}
                GET /api/clips/export · GET /api/clips/{clipId}/analysis · POST .../analysis/regenerate
Recordings      POST /api/clips/{clipId}/recordings · GET .../recordings · GET·DELETE /api/recordings/{id}/audio
Review (SM-2)   GET /api/review/queue · POST /api/review/items/{id}/respond · GET /api/review/streak
Practice        GET /api/practice/progress · POST /api/practice/rep
                GET /api/practice/srs · POST /api/practice/srs/grade · POST /api/practice/compose/check
Prepositions    GET /api/prepositions/mined
Decks           POST·GET /api/decks · PATCH·DELETE /api/decks/{deckId} · PATCH /api/decks/clips/{clipId}
Collections     GET /api/collections · GET /api/collections/{idOrSlug}
Health          GET /api/health

Run it locally

Prereqs: Docker, Java 21, Node 22+ (Next.js 16 needs Node 22).

# 1. PostgreSQL (port 5434)
docker compose up -d
 
# 2. Backend → http://localhost:8080   (JAVA_HOME must point at a JDK 21)
cd backend
cp ../.env.example ../.env          # JWT secret; an LLM API key is optional
./gradlew bootRun
 
# 3. Frontend → http://localhost:3100
cd frontend
npm install
npm run dev

The core flow (import → clip → review → record) and all Practice drills run without any API key. Add a Gemini or Claude key to .env to enable AI analysis and the composition check. One-command full stack (DB + backend with a JVM debug port + frontend): docker compose -f docker-compose.dev.yml up.


Deployment

Two supported models — the same image/app adapts to both.

1) Serverless — Vercel + AWS (target of the live deploy) Frontend on Vercel (mimi.daeseon.ai); backend container on AWS ECS Fargate behind an ALB; RDS PostgreSQL (ca-central-1); recordings on S3 (private bucket, streamed through the backend). CI/CD is keyless: GitHub Actions assumes an IAM role via OIDC (deploy.yml) → builds + pushes to ECR → rolls the ECS service. A step-by-step first-time runbook lives in infrastructure/aws-bootstrap.md, with the task definition in infrastructure/ecs-task-definition.json.

2) Persistent host — Render / Fly / a VM The multi-stage Dockerfile (JRE + yt-dlp + ffmpeg) reads PORT for PaaS injection and runs anywhere a container runs; pair it with any managed Postgres + S3/R2.


Testing

cd backend  && JAVA_HOME=<jdk21> ./gradlew test   # 26 classes — JUnit 5 + Testcontainers (real PostgreSQL)
cd frontend && npm test                            # Vitest units (incl. the SRS session logic)
cd e2e      && npx playwright test                 # 14 specs (auth, clip, quizzes, decks, i18n, BYOAI, mobile, …)

Engineering log

This repo keeps a disciplined, anti-hallucination log — every non-trivial fix or decision is dual-written, with literal error text, verified causes, and post-commit hashes.


Honest limitations

Stated plainly — knowing the edges is part of the engineering.


Project layout

backend/                Spring Boot — 8 bounded contexts + common, 17 Flyway migrations
  com/tubeshadow/
    auth/               JWT + token-version revocation · @CurrentUser resolver · IP rate limiter
    video/ clip/        YouTube import (yt-dlp) · clips · transcript · notes · decks · export
    analysis/           AiAnalysisClient (Gemini/Claude) · async pipeline · prompt + parser · retry
    recording/          local↔S3 storage (env-gated) · streamed audio route
    review/             SM-2 scheduler (pure) · 3 quiz modes
    practice/           Leitner SRS · streak · collocations/patterns/prepositions · AI compose + per-user limiter
    common/             ApiResponse · exceptions · WebMvc/async/S3 config · MDC logging
frontend/               Next.js 16 App Router · 5-locale i18n · shadcn/ui
  app/[locale]/(app)/   library · player · review · import · discover · prepositions
                        practice · patterns · collocations · compose · weak · settings
  lib/                  api client · practice-srs (session builder) · stores · hooks
e2e/                    Playwright specs (14)
infrastructure/         AWS ECS task definition + a first-time bootstrap runbook
docs/                   troubleshooting log (problem → cause → fix → commit)
content/logs/           dated build-log entries (aggregated live by daeseon.ai)

Mimi — input → output → repeat. Built to remove friction, not add features.

Repo: Daeseon-AI-Factory/shadow-ai · Build log: daeseon.ai/projects/shadow-ai · 한국어 README

License

MIT