Design / Web/Discite
18 / 67

12/2023Design / Web

Discite

╌╌╌╌

Discite turns idle screen time into learning: a short-form video app that teaches computer science fundamentals through bite-sized, swipeable clips, built for the way people actually spend an idle minute. I built it as my Dartmouth CS98 senior capstone, and it grew into a five-part platform: a SwiftUI iOS client, a TypeScript / Express API on MongoDB, a Python ML service that cuts long lectures into clips, a recommendation engine over a vector index, and an AWS streaming tier.

End to end, the SwiftUI client speaks only to the Express API, which persists state in MongoDB and queries a Pinecone vector index for related clips. The ML service cuts lectures into clips and registers their metadata with the API; the recommendation engine ranks candidates; S3 and CloudFront stream the signed HLS bytes back to the app.

The app is SwiftUI, organized MVVM with a Views / ViewModels / Models / Service split per feature. Authentication obtains a JWT and stores it in the Keychain (KeychainItem), with a Google sign-in path alongside email. Watch is the core surface: an IGList-backed feed (PlayerView, EmbeddedVideoCell, NavigationDotsView) where a SwipeDirection enum reads the drag gesture, sideways swipes stepping through a topic's clips and a downward swipe advancing to the next topic, all over a CustomVideoPlayer. Explore drives topics, playlists, and search; Account holds profiles and a Friends graph. The client keeps no business logic; it is a consumer of the REST API.

The backend follows Express's model–controller–router layout in TypeScript, with Passport guarding routes by JWT (requireAuth, requireSignin, requireAdmin). Mongoose maps a small set of collections:

user : names, lowercase-unique email and username, bcrypt password, savedPlaylists, isAdmin, email verification

video_metadata : title, youtubeURL, topicId[], clips[], views / likes / dislikes sets, isVectorized, isClipped

clip_metadata : videoId, duration, thumbnailURL, clipURL pointing at a CDN manifest, and the same engagement sets

user_affinity : per-topic affinities and complexities maps in [0, 1], plus a bounded activeAffinities buffer of recent watches

Routes cover auth (/auth/signup, /auth/signin), the social graph (/relationships, /connections/:userId), engagement (GET /videos/:videoId, nested comment threads, and like / dislike / toohard / tooeasy, each of which nudges the caller's affinity), and recommendations. Around fifty Cypress specs exercise it end to end (affinity, recommendation, vectorized-rec, watch-history, search, user, video).

Turning lectures into clips is the job of the ML service, a Dockerized FastAPI app. Its /split route runs process_video, pulling a YouTube video's frames and transcript, then labels both against a topic set: CLIP (clip-vit-base-patch32) scores each frame and BART (bart-large-mnli) does zero-shot classification on the transcript around each second. That yields a per-second topic time-series, which a tfidf pass reweights so keywords common to the whole video (SELECT throughout a SQL lecture) count for less than distinctive ones. A sliding-window change detector then compares each window's topic mixture against a running mean or median; when the gap crosses a threshold it marks a clip boundary. Raw segments go to S3 and their metadata is PUT to the API.

Choosing what to play falls to the engine, a separate FastAPI service over a Pinecone cosine index (namespace video-transcripts) and an Algolia search index. Candidate generation queries Pinecone for videos near a seed clip; ranking then reorders them by taste, as VideoRanker adds each viewer's per-topic affinity to the similarity score and re-sorts. The backend also queries Pinecone directly for its vectorized feed. Two signals from the affinity model, interest and difficulty, together with vector-space topic similarity, decide the next clip, and every like, dislike, toohard, or tooeasy event feeds back into the scores so the sequence adapts as you learn.

Clips never stream from the database. Bytes live on Amazon S3 and reach the app over HLS through CloudFront. When the client wants a clip it asks the API for a signed .m3u8; a Lambda fetches and caches the CloudFront private key, checks that the request is authorized, and signs the manifest, appending signed params to every .ts segment so playback loads progressively. The public docs and marketing site is a separate Nuxt Content app.

Read more in the Medium write-up and the project's architecture docs.

References

  1. Project repository
  2. Live site

╌╌ END ╌╌