# Project export: Vidscribe

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: An interactive study notebook and video lecture annotation tool
- Devpost: https://devpost.com/software/vid-scribe
- GitHub: https://github.com/adrian-perez121/VidScribe
- Demo: https://vidscribe-vdk5.onrender.com/
- Video: https://www.youtube.com/embed/fTBgzcah3Ho?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Adrian Perez (22 commits), Halren (20 commits), David (19 commits), Claude Sonnet 4.6 (15 commits), angelinedo (1 commits)

## Devpost submission (written by the team)

### Inspiration

If students can write notes in the margins of books and PDFs, why not do the same for video lectures? Students increasingly use AI transcript tools to convert lectures, class recordings, and educational videos into notes. But current systems have several problems: AI transcripts frequently misinterpret words because of accents, technical vocabulary, poor audio quality, fast speech, or speaker pronunciation. Students use little to no effort in learning when using AI transcribing and note-taking tools, leading them to have insufficient knowledge. Important visual content—diagrams, equations, slides, charts, demonstrations—often gets lost entirely. Students constantly pause and rewind lectures to manually write notes. Notes become disconnected from the original explanation, making it difficult to revisit concepts later.

### What it does

With Vidscribe, students can create notes attached directly to any timestamp while watching a lecture or educational video. Students can also annotate directly on the video using a select-and-annotate tool, allowing them to highlight important concepts, diagrams, equations, images, or specific areas on the screen. To improve accessibility, Vidscribe also includes a speech-to-text microphone feature powered by Deepgram, allowing students to verbally record notes instead of typing. This is especially beneficial for students with disabilities, including dyslexic students, by making note-taking faster and more accessible. If students need additional clarification on a topic, an integrated AI agent is available to answer questions in real time. Using the select tool, students can highlight equations, diagrams, images, or sections of the video and ask for further explanation or context. Once the student finishes watching the video, Vidscribe generates a transcript and combines it with the student’s annotations, timestamped notes, spoken notes, and AI-provided explanations. This information is then automatically compiled into organized study notes, creating a personalized study guide directly connected to the original lecture. Students can also generate flashcards, review questions, and other study materials from their compiled notes to reinforce learning and improve retention.

### How we built it

To build Vidscribe, we combined several AI and infrastructure tools into one seamless, intelligent learning experience. Deepgram — we used Deepgram's Speech-to-Text API to accurately transcribe lectures into timestamped text that merges with student notes and annotations. It also powers the microphone feature, letting students speak their notes instead of typing — improving accessibility for students with disabilities, including dyslexic learners. Deepgram — we used Deepgram's Speech-to-Text API to accurately transcribe lectures into timestamped text that merges with student notes and annotations. It also powers the microphone feature, letting students speak their notes instead of typing — improving accessibility for students with disabilities, including dyslexic learners. Anthropic Claude — we used Claude Code to accelerate development across the front-end and back-end, enabling rapid prototyping and feature implementation. At runtime, the Claude API powers the product's intelligence: explaining screenshots captured from videos, answering questions in the study chatbot, and generating study guides and flashcards — all grounded strictly in the student's own notes and transcripts so it never invents facts. Anthropic Claude — we used Claude Code to accelerate development across the front-end and back-end, enabling rapid prototyping and feature implementation. At runtime, the Claude API powers the product's intelligence: explaining screenshots captured from videos, answering questions in the study chatbot, and generating study guides and flashcards — all grounded strictly in the student's own notes and transcripts so it never invents facts. Redis — Redis 8 is the backbone of our study layer. We use it as a vector database (RediSearch) to embed and semantically search a student's notes, transcripts, research, and visual explanations, powering a retrieval-augmented chatbot that cites the exact video and timestamp an answer came from. Redis also stores per-session conversation memory, caches generated study guides and answers for instant reuse, and schedules spaced-repetition flashcards with sorted sets so "what's due now" is an O(log n) lookup. Redis — Redis 8 is the backbone of our study layer. We use it as a vector database (RediSearch) to embed and semantically search a student's notes, transcripts, research, and visual explanations, powering a retrieval-augmented chatbot that cites the exact video and timestamp an answer came from. Redis also stores per-session conversation memory, caches generated study guides and answers for instant reuse, and schedules spaced-repetition flashcards with sorted sets so "what's due now" is an O(log n) lookup. OpenAI — we use OpenAI's text-embedding-3-small model to turn notes and transcript chunks into vector embeddings, which Redis indexes for fast semantic retrieval. OpenAI — we use OpenAI's text-embedding-3-small model to turn notes and transcript chunks into vector embeddings, which Redis indexes for fast semantic retrieval. Browserbase — we integrated Browserbase to search the web for resources related to lecture topics, identifying key concepts, visiting relevant pages, and generating concise summaries. This encourages students to explore further without leaving the lecture page, reducing opportunities for distraction. Browserbase — we integrated Browserbase to search the web for resources related to lecture topics, identifying key concepts, visiting relevant pages, and generating concise summaries. This encourages students to explore further without leaving the lecture page, reducing opportunities for distraction. Google Gemini — we used the Gemini API to condense larger chunks of text into concise 5–6 word topic labels, which are then passed to Browserbase for more targeted searches. Google Gemini — we used the Gemini API to condense larger chunks of text into concise 5–6 word topic labels, which are then passed to Browserbase for more targeted searches. MongoDB — we used MongoDB (with GridFS) to store uploaded videos, timestamped notes, annotations, transcripts, and flashcards, enabling efficient organization and retrieval across learning sessions. MongoDB — we used MongoDB (with GridFS) to store uploaded videos, timestamped notes, annotations, transcripts, and flashcards, enabling efficient organization and retrieval across learning sessions. Hono — a lightweight Hono server on Node.js runs the backend API, with a React + Vite + TypeScript front-end. Students can also export their notes and study guides to Word (.docx). Hono — a lightweight Hono server on Node.js runs the backend API, with a React + Vite + TypeScript front-end. Students can also export their notes and study guides to Word (.docx).

### Challenges we ran into

During development, we encountered several technical and workflow challenges. One of our biggest issues was unreliable Wi-Fi connectivity, which prevented us from consistently connecting to our database server and forced us to temporarily rely on mobile hotspots to continue development. We also spent time deciding between using a SQL or NoSQL database architecture. Since Vidscribe needs to manage larger files and flexible user-generated content such as annotations, notes, and media-related data, we ultimately chose MongoDB because its document-based structure better fit our needs. Another challenge involved Redis Cloud, which unexpectedly stopped working during development. Initially, we struggled to identify the problem because an important update regarding the issue had been posted earlier in Slack and was easy to miss. We also encountered difficulties with AI development workflows, particularly when running out of conversation context during coding sessions, which caused us to lose parts of our progress and planning history. This required us to spend additional time reconstructing our ideas and reestablishing development context before moving forward.

### Accomplishments we're proud of

We are proud that we successfully achieved the core goals we initially scoped for Vidscribe and were able to turn our idea into a functional product within a limited timeframe. Throughout development, our team maintained a smooth workflow and experienced very few merge conflicts, which allowed us to spend more time building features rather than resolving technical issues. We were also able to successfully integrate a strong variety of sponsor tools into the project, leveraging their capabilities to enhance functionality and create a more intelligent and seamless user experience. Most importantly, our collaboration remained efficient throughout the project, with strong communication and teamwork allowing us to rapidly iterate on ideas and overcome challenges together.

### What we learned

Throughout the development of Vidscribe, we learned how to integrate and coordinate multiple APIs within a single application to create a seamless user experience. Since our project relied on several tools and services working together, we gained experience connecting AI models, speech-to-text systems, memory services, databases, and web automation tools into one workflow. We also learned how important it is to design systems where each tool has a specific role and can effectively communicate with others. Beyond the technical side, we gained experience in debugging integrations, managing development workflows, and collaborating as a team while building a project under time constraints.

### What's next

Moving forward, we plan to expand Vidscribe beyond educational lectures into film studies and media analysis, allowing students to use timestamped annotations to analyze scenes, themes, dialogue, and cinematography. We also plan to develop Vid Scribe into a Chrome extension that integrates with platforms like Canvas and other video-hosting sites, making it easier to use within existing learning environments.

## README (from the GitHub repository)

# Vidscribe

A video annotation tool that lets you take notes on video lectures and automatically research concepts from the transcript using live web sources. Notes are timestamped to the exact moment in the video, giving you a study notebook that remembers where every idea came from. On top of that, a Redis-powered study layer turns your notes and transcripts into a RAG chatbot, study guides, and spaced-repetition flashcards — all exportable to Word. Demo video [here](https://www.youtube.com/watch?v=fTBgzcah3Ho).

## Features

**Text notes** — write a note at any playback position; the timestamp is saved automatically.

**Voice notes** — record a quick observation via microphone; Deepgram transcribes it and the text is saved as a note.

**Visual notes** — draw a crop box over any frame; Claude explains what is in that region in plain language.

**AI research** — highlight a chunk of transcript; the server distills keywords, searches the web via Browserbase, and returns a teacher-style summary plus the source links. The summary is explicitly tied back to what is on screen.

**Lecture transcripts** — Deepgram transcribes the full video into timestamped segments, stored once per video and reused by the study tools below.

**Study chatbot** — ask questions across your videos. A Redis vector search (RAG) retrieves the most relevant notes, research summaries, visual explanations, and transcript chunks; Claude answers grounded in them and returns the source video(s) — with a timestamp to jump to when the answer comes from the transcript. Conversation memory and first-turn answers are kept in Redis; if nothing relevant is found it says so instead of inventing an answer.

**Study guide** — generate a structured guide (overview + themed sections) from a single video or your whole library, drawn from notes, visual explanations, research, and transcript. Cached in Redis (invalidated when notes change) with a one-click Regenerate.

**Flashcards with spaced repetition** — auto-generate question/answer cards from the same material and review them with the SM-2 algorithm. Due-date scheduling is backed by a Redis sorted set, so "what's due now" is an O(log n) lookup.

**DOCX export** — download all your notes (with their summaries and timestamps) or the generated study guide as Word documents.

**Video library** — upload MP4 or WebM files to a MongoDB GridFS store. The dashboard shows thumbnails (captured in-browser before upload), durations, and per-video note counts. Videos can be deleted and their notes are removed from both the database and localStorage.

**Demo mode** — a bundled lecture video works with no credentials at all (notes go to localStorage only).

## Tech stack

| Layer | Technology |
|---|---|
| Frontend | React 19 + Vite + TypeScript + Tailwind CSS |
| Backend | Hono on Node.js (run with tsx, no compile step) |
| Video storage | MongoDB Atlas + GridFS (streaming, range-request aware) |
| Notes | localStorage (live store) + MongoDB (mirrored for persistence) |
| Speech-to-text | Deepgram Nova |
| Visual explain / chatbot / study guide / flashcards | Anthropic Claude (`claude-sonnet-4-6`) |
| Web research | Browserbase + Stagehand + Gemini |
| Vector search, caching, session memory, due-queue | Redis 8 (RediSearch / sorted sets) |
| Embeddings | OpenAI `text-embedding-3-small` (1536-dim) |
| Document export | `docx` |

## Requirements

- Node.js 20+ (22 recommended)
- MongoDB Atlas cluster (or any MongoDB 6+ instance)
- Redis 8 with the search module — local `redis-server` 8.x or Redis Stack (powers the chatbot, caching, and flashcard due-queue)
- API keys: Anthropic, OpenAI, Deepgram, Browserbase, Gemini (see below)

## Setup

```sh
npm install
cp packages/server/.env.sample packages/server/.env
```

Fill in `packages/server/.env`:

```
# MongoDB
MONGODB_URI="mongodb+srv://..."
MONGODB_USERNAME="..."
MONGODB_PASSWORD="..."
MONGODB_DB="vidmark"

# AI services
ANTHROPIC_API_KEY="sk-ant-..."
OPENAI_API_KEY="sk-proj-..."   # embeddings for the chatbot/study tools
DEEPGRAM_API_KEY="..."
GEMINI_API_KEY="..."
BROWSERBASE_API_KEY="..."

# Redis (vector index, caches, sessions, due-queue)
REDIS_URL="redis://localhost:6379"
```

If you want to run without external API keys during a demo, set the mock flags:

```
MOCK_DEEPGRAM="true"       # voice notes return a canned transcript
MOCK_BROWSERBASE="true"    # research returns a placeholder summary
```

## Running

```sh
# Development (hot reload on both frontend :5173 and server :3000)
npm run dev

# Production (build then serve everything from :3000)
npm run build
npm run start
```

In development the Vite dev server proxies `/api` to the Hono server, so there is no CORS to configure. In production the Hono server serves the built frontend and the API from the same origin.

## Redis & the search index

The study chatbot searches a Redis vector index (`idx:study`) of your notes,
research, visual explanations, and transcript chunks. Caches, chat session
memory, and the flashcard due-queue also live in Redis.

Run Redis 8 locally (Ubuntu example — needs the bundled `search` module):

```sh
redis-cli ping          # PONG
redis-cli MODULE LIST   # should include "search"
```

The chatbot reads from the index, which is built by a batch script. After adding
notes / generating transcripts, (re)build it:

```sh
cd packages/server
npx tsx scripts/ingest.ts      # embeds notes + transcripts into idx:study
npx tsx scripts/healthcheck.ts # checks Redis + OpenAI + Claude connectivity
```

Re-running `ingest.ts` is safe — it drops and rebuilds the index from MongoDB and
clears the dependent caches. The study guide, flashcards, and DOCX exports read
notes from MongoDB directly, so they reflect changes immediately without a
re-ingest; only the chatbot depends on the index.

## Project layout

```
packages/
  web/      React frontend (src/pages, src/components, src/lib)
  server/   Hono API (src/routes, lib/)
  shared/   TypeScript types imported by both (no build step)
```

## API overview

| Method | Path | Description |
|---|---|---|
| GET | /api/health | Server status |
| POST | /api/explain | Explain a cropped video frame (multipart: image + prompt) |
| POST | /api/deepgram/voice-note | Transcribe audio (multipart: audio blob) |
| POST | /api/research | Research a transcript chunk; returns keywords + summary + links |
| GET | /api/videos | List all uploaded videos (metadata + thumbnails) |
| GET | /api/videos/:id | Video metadata + its notes |
| GET | /api/videos/:id/stream | Range-aware video stream (for `<video>` playback) |
| POST | /api/videos | Upload a video (multipart: title + thumbnail + file) |
| DELETE | /api/videos/:id | Delete video and all associated notes |
| POST | /api/notes | Upsert a note |
| DELETE | /api/notes/:id | Delete a note |
| POST | /api/videos/:id/transcript | Transcribe the video via Deepgram and store it |
| GET | /api/videos/:id/transcript | Fetch the stored transcript |
| GET | /api/videos/:id/transcript/window | Transcript segments around a timestamp |
| POST | /api/chat | Study chatbot — `{ message, session_id?, video_id? }` → `{ answer, sources }` |
| POST | /api/study-guide | Generate a study guide — `{ video_id?, refresh? }` |
| POST | /api/flashcards/generate | Generate + persist flashcards — `{ video_id?, count? }` |
| GET | /api/flashcards | List cards (`?video_id=&due=true`; `due` uses the Redis queue) |
| POST | /api/flashcards/:id/review | Review a card (SM-2) — `{ grade }` |
| GET | /api/export/notes.docx | Download all notes (+ summaries + timestamps) as Word |
| GET | /api/export/study-guide.docx | Download the study guide as Word |

## License

MIT


## Detected evidence (automated analysis)

Indexed codebase: 59 recognized source files, 225 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- MongoDB (technology) — detected in the code
- OpenAI (technology) — detected in the code
- PostgreSQL (technology) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (73 of 73)

```
.gitignore
package.json
packages/server/.env.sample
packages/server/.gitignore
packages/server/FLASHCARDS.md
packages/server/lib/anthropic.ts
packages/server/lib/cache.ts
packages/server/lib/contentCollector.ts
packages/server/lib/deepgram.ts
packages/server/lib/dueQueue.ts
packages/server/lib/exportDocx.ts
packages/server/lib/flashcards.ts
packages/server/lib/gemini.ts
packages/server/lib/mongo.ts
packages/server/lib/prisma.ts
packages/server/lib/redis.ts
packages/server/lib/research.ts
packages/server/lib/search.ts
packages/server/lib/session.ts
packages/server/lib/sm2.ts
packages/server/lib/stagehand.ts
packages/server/lib/studyGuide.ts
packages/server/package.json
packages/server/prisma.config.ts
packages/server/prisma/migrations/20260620232958_init/migration.sql
packages/server/prisma/migrations/migration_lock.toml
packages/server/prisma/schema.prisma
packages/server/prisma/seed.ts
packages/server/scripts/healthcheck.ts
packages/server/scripts/ingest.ts
packages/server/scripts/mongo-check.ts
packages/server/scripts/research-example.ts
packages/server/scripts/stagehand-example.ts
packages/server/scripts/verify-prisma.ts
packages/server/scripts/verify-stagehand.ts
packages/server/src/index.ts
packages/server/src/routes/api.ts
packages/server/src/routes/chat.ts
packages/server/src/routes/deepgram.ts
packages/server/src/routes/explain.ts
packages/server/src/routes/export.ts
packages/server/src/routes/flashcards.ts
packages/server/src/routes/notes.ts
packages/server/src/routes/studyGuide.ts
packages/server/src/routes/videos.ts
packages/server/STUDY_CHATBOT.md
packages/server/STUDY_GUIDE.md
packages/server/tsconfig.json
packages/shared/package.json
packages/shared/src/index.ts
packages/shared/tsconfig.json
packages/web/eslint.config.js
packages/web/index.html
packages/web/package.json
packages/web/src/App.tsx
packages/web/src/components/AppHeader.tsx
packages/web/src/components/ChatWidget.tsx
packages/web/src/components/UploadButton.tsx
packages/web/src/components/VideoWorkspace.tsx
packages/web/src/index.css
packages/web/src/lib/api.ts
packages/web/src/lib/theme.tsx
packages/web/src/lib/thumbnail.ts
packages/web/src/main.tsx
packages/web/src/pages/Dashboard.tsx
packages/web/src/pages/Home.tsx
packages/web/src/pages/VideoPage.tsx
packages/web/src/vite-env.d.ts
packages/web/tsconfig.app.json
packages/web/tsconfig.json
packages/web/tsconfig.node.json
packages/web/vite.config.ts
README.md
```

### Dependencies

- package.json: @smithy/eventstream-codec@^4.4.1, @tailwindcss/vite@^4.3.1, set-cookie-parser@^3.1.0, tailwindcss@^4.3.1, tough-cookie@^6.0.1, tr46@^6.0.0
- packages/server/package.json: @anthropic-ai/sdk@^0.105.0, @browserbasehq/stagehand@^3.6.0, @deepgram/sdk@^5.4.0, @google/genai@^1.52.0, @hono/node-server@^1.13.0, @prisma/adapter-pg@^7.8.0, @prisma/client@^7.8.0, @types/busboy@^1.5.4, @types/node@^22.20.0, @types/pg@^8.20.0, @vid-mark/shared@*, busboy@^1.6.0, docx@^9.7.1, dotenv@^17.4.2, fast-safe-stringify@^2.1.1, hono@^4.6.0, keyword-extractor@^0.0.28, mongodb@^7.3.0, openai@^6.44.0, pg@^8.22.0, playwright-core@^1.61.0, prisma@^7.8.0, redis@^6.0.0, sharp@^0.35.2, tsx@^4.22.4, typescript@^5.6.0, zod@^4.4.3
- packages/web/package.json: @eslint/js@^9.0.0, @types/react@^19.1.12, @types/react-dom@^19.1.9, @typescript-eslint/eslint-plugin@^8.0.0, @typescript-eslint/parser@^8.0.0, @vid-mark/shared@*, @vitejs/plugin-react@^5.0.0, eslint@^9.0.0, eslint-plugin-react-hooks@^5.0.0, eslint-plugin-react-refresh@^0.4.14, globals@^15.0.0, react@^19.1.1, react-dom@^19.1.1, react-router-dom@^7.18.0, typescript@^5.6.0, typescript-eslint@^8.0.0, vite@^6.3.5

### Recent commits (newest first)

- Update README.md
- Update README for chatbot, study guide, flashcards, transcripts, exports, and Redis
- UI: recolor workspace buttons and make the left column scroll
- Add Redis sorted-set due-queue for flashcards
- Add study guide regenerate (cache bypass)
- Merge branch 'redis-cache'
- Cache study guides and first-turn chat answers in Redis
- Merge branch 'docx-export'
- Add .docx export for notes and study guide
- Add mobile functionality
- Fix variable issue
- Add flashcards panel
- Merge branch 'main' of github.com:adrian-perez121/vid-mark
- Add light and dark mode
- Add study guide panel
- Auto-generate transcripts after upload
- Document study guide and flashcards features
- Merge branch 'study-guide-flashcards'
- Add study guide + flashcards, and feed transcripts to the chatbot
- Add transcript viewer panel

## Key source files (fetched from GitHub, selected and truncated for size)

### packages/server/STUDY_GUIDE.md

```markdown
# Study guide generator

Generates a structured study guide on demand from a student's collected material —
their notes, AI ("lens") explanations, web-research summaries, and the lecture
transcript. Built fresh on every request and returned; **not persisted**.

Scope is optional: pass a `video_id` to build a guide for one video, or omit it to
build one across the whole library.

---

## Endpoint

### `POST /api/study-guide`

**Body**
```jsonc
{ "video_id": "6a37e6a8b5d91bd3c3367bd0" }  // optional — omit for the whole library
```

**Response** `{ guide }`
```jsonc
{
  "guide": {
    "title": "Study guide: videoplayback (1) (1)",   // or "Study guide: all videos"
    "overview": "A 2-4 sentence summary of what the material covers.",
    "sections": [
      { "heading": "Energy Storage and ATP", "points": ["...", "..."] },
      { "heading": "Redox Reactions",        "points": ["...", "..."] }
    ],
    "videoId": "6a37e6a8b5d91bd3c3367bd0"            // present only when scoped
  }
}
```

**Errors**
- `404` — no notes or transcript exist yet to build from.
- `500` — generation failed (see server logs).

---

## How it works

1. **Collect material** (`lib/contentCollector.ts → collectContent`): pulls straight
   from MongoDB (no vector search), aggregating per source and labeling each block:
   - `note` ← `note.text`
   - `browserbase` ← `note.researchSummary`
   - `lens` ← `note.aiExplanation`
   - `transcript` ← the video's stored transcript (joined segments, capped at
     `MAX_TRANSCRIPT_CHARS = 12000` per video so a long lecture can't blow the prompt)
2. **Generate** (`lib/studyGuide.ts → generateStudyGuide`): sends the labeled material
   to Claude (`claude-sonnet-4-6`) with a system prompt that requires **JSON-only**
   output (`overview` + `sections[]`), grounded strictly in the material — no invented
   facts. The response is parsed with a tolerant JSON extractor.
3. **Return** the normalized guide. Empty/invalid sections are filtered out.

Returns `null` (→ `404`) when there's no material for the requested scope.

---

## Files

| File | Role |
|---|---|
| `src/routes/studyGuide.ts` | `POST /api/study-guide` route |
| `lib/studyGuide.ts` | `generateStudyGuide(videoId?)` |
| `lib/contentCollector.ts` | `collectContent(videoId?)` — shared with flashcards |
| `lib/anthropic.ts` | shared Claude client + JSON extraction helpers |
| `packages/shared/src/index.ts` | `StudyGuide`, `StudyGuideSection`, `StudyGuideRequest`, `StudyGuideResponse` |

---

## Requirements
- `ANTHROPIC_API_KEY`, `MONGODB_URI` in `packages/server/.env`.
- Notes and/or a transcript stored in Mongo for the requested scope. (Transcripts are
  created via `POST /api/videos/:id/transcript`.)
- Does **not** use Redis — it reads source material directly from Mongo.

---

## Try it

```bash
# whole library
curl -s -X POST http://localhost:3000/api/study-guide \
  -H 'Content-Type: application/json' -d '{}'

# one video
curl -s -X POST http://localhost:3000/api/study-guide \
  -
[truncated — 219 more characters]
```

### packages/server/FLASHCARDS.md

```markdown
# Flashcards (SM-2 spaced repetition)

Generates question/answer flashcards from the same material as the study guide
(notes, lens explanations, research summaries, transcript), then **persists** them in
MongoDB so spaced-repetition scheduling survives across sessions. Reviews are graded
Anki-style and rescheduled with the **SM-2** algorithm.

Scope is optional: pass a `video_id` to build a deck for one video, or omit it for the
whole library.

---

## Endpoints

### `POST /api/flashcards/generate`
Generate and persist a deck.

**Body**
```jsonc
{
  "video_id": "6a37e6a8b5d91bd3c3367bd0",  // optional — omit for the whole library
  "count": 12                              // optional — clamped to 1..30 (default 12)
}
```
**Response** `{ cards: Flashcard[] }` — `404` if there's no material to build from.

> Regenerate semantics: a **scoped** (`video_id`) generate first deletes that video's
> existing cards, then inserts the new ones. Whole-library generation **appends**.

### `GET /api/flashcards?video_id=<id>&due=true`
List stored cards, sorted by `dueAt` ascending.
- `video_id` (optional) — scope to one video.
- `due=true` (optional) — only cards due now (`dueAt <= now`).

**Response** `{ cards: Flashcard[] }`

### `POST /api/flashcards/:id/review`
Grade a card and reschedule it via SM-2.

**Body** `{ "grade": "again" | "hard" | "good" | "easy" }`
**Response** `{ card: Flashcard }` — the updated, rescheduled card. `404` if not found,
`400` on an invalid grade.

---

## Flashcard shape

```jsonc
{
  "id": "c32900f6-...",
  "videoId": "6a37e6a8b5d91bd3c3367bd0",   // absent for whole-library cards
  "front": "What does OIL RIG stand for in redox reactions?",
  "back": "Oxidation Is Loss, Reduction Is Gain — ...",
  "ease": 2.5,            // SM-2 ease factor (>= 1.3)
  "intervalDays": 1,      // current inter-review interval in days
  "repetitions": 1,       // consecutive successful reviews
  "dueAt": "2026-06-22T15:03:59.584Z",
  "createdAt": "2026-06-21T15:03:59.181Z",
  "lastReviewedAt": "2026-06-21T15:03:59.584Z"
}
```

---

## SM-2 scheduling (`lib/sm2.ts`)

New cards start at `ease = 2.5`, `intervalDays = 0`, `repetitions = 0`, and are due
immediately. On review, the grade maps to an SM-2 quality score:

| Grade  | Quality | Effect |
|--------|---------|--------|
| again  | 1 | **Fail**: `repetitions → 0`, re-shown in ~10 minutes, ease decreased |
| hard   | 3 | Advance, but ease decreases |
| good   | 4 | Advance, ease ~unchanged |
| easy   | 5 | Advance, ease increases |

On a passing grade (`quality >= 3`):
- `repetitions += 1`
- interval becomes **1 day** (1st rep), **6 days** (2nd rep), then
  `round(previousInterval × ease)` thereafter.
- `dueAt = now + intervalDays`.

Ease updates by the standard SM-2 formula and is floored at `1.3`. `dueAt` is stored as
an ISO-8601 UTC string, so the `due=true` filter is a simple lexicographic `<=`
comparison (which equals chronological order).

---

## Storage

MongoDB collection **`flashcards`**, 
[truncated — 1628 more characters]
```

### package.json

```
{
  "name": "vid-mark",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev": "npm run dev --workspace=@vid-mark/server & npm run dev --workspace=@vid-mark/web",
    "dev:web": "npm run dev --workspace=@vid-mark/web",
    "dev:server": "npm run dev --workspace=@vid-mark/server",
    "build": "npm run build --workspace=@vid-mark/web && npm run build --workspace=@vid-mark/server",
    "start": "npm run start --workspace=@vid-mark/server",
    "lint": "npm run lint --workspace=@vid-mark/web"
  },
  "dependencies": {
    "@smithy/eventstream-codec": "^4.4.1",
    "@tailwindcss/vite": "^4.3.1",
    "set-cookie-parser": "^3.1.0",
    "tailwindcss": "^4.3.1",
    "tough-cookie": "^6.0.1",
    "tr46": "^6.0.0"
  }
}

```

### packages/shared/package.json

```
{
  "name": "@vid-mark/shared",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "types": "./src/index.ts"
}

```

### packages/web/package.json

```
{
  "name": "@vid-mark/web",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@vid-mark/shared": "*",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "react-router-dom": "^7.18.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.0.0",
    "@types/react": "^19.1.12",
    "@types/react-dom": "^19.1.9",
    "@typescript-eslint/eslint-plugin": "^8.0.0",
    "@typescript-eslint/parser": "^8.0.0",
    "@vitejs/plugin-react": "^5.0.0",
    "eslint": "^9.0.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "globals": "^15.0.0",
    "typescript": "^5.6.0",
    "typescript-eslint": "^8.0.0",
    "vite": "^6.3.5"
  }
}

```

### packages/server/package.json

```
{
  "name": "@vid-mark/server",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc --noEmit",
    "start": "tsx src/index.ts"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@browserbasehq/stagehand": "^3.6.0",
    "@deepgram/sdk": "^5.4.0",
    "@google/genai": "^1.52.0",
    "@hono/node-server": "^1.13.0",
    "@prisma/adapter-pg": "^7.8.0",
    "@prisma/client": "^7.8.0",
    "@vid-mark/shared": "*",
    "busboy": "^1.6.0",
    "docx": "^9.7.1",
    "dotenv": "^17.4.2",
    "fast-safe-stringify": "^2.1.1",
    "hono": "^4.6.0",
    "keyword-extractor": "^0.0.28",
    "mongodb": "^7.3.0",
    "openai": "^6.44.0",
    "pg": "^8.22.0",
    "playwright-core": "^1.61.0",
    "redis": "^6.0.0",
    "sharp": "^0.35.2",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@types/busboy": "^1.5.4",
    "@types/node": "^22.20.0",
    "@types/pg": "^8.20.0",
    "prisma": "^7.8.0",
    "tsx": "^4.22.4",
    "typescript": "^5.6.0"
  }
}

```

### packages/web/src/App.tsx

```typescript
import { Routes, Route } from 'react-router-dom'
import Dashboard from './pages/Dashboard'
import VideoPage from './pages/VideoPage'
import ChatWidget from './components/ChatWidget'

function App() {
  return (
    <>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/videos/:id" element={<VideoPage />} />
      </Routes>
      <ChatWidget />
    </>
  )
}

export default App

```

### packages/web/src/main.tsx

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
import { ThemeProvider } from './lib/theme.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ThemeProvider>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </ThemeProvider>
  </React.StrictMode>,
)

```

### packages/server/src/index.ts

```typescript
import 'dotenv/config'
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { api } from './routes/api.js'

console.log(`ANTHROPIC_API_KEY set: ${Boolean(process.env.ANTHROPIC_API_KEY)}`)

const app = new Hono()

app.use('*', logger())

// All backend endpoints live under /api (matches the Vite dev proxy).
app.route('/api', api)

// In production, serve the built React frontend from the web package.
// In dev you don't need this — Vite serves the frontend on :5173 and
// proxies /api here.
if (process.env.NODE_ENV === 'production') {
  app.use('/*', serveStatic({ root: '../web/dist' }))
  // SPA fallback: any non-API route returns index.html.
  app.get('*', serveStatic({ path: '../web/dist/index.html' }))
}

const port = Number(process.env.PORT ?? 3000)
serve({ fetch: app.fetch, port }, (info) => {
  console.log(`Server listening on http://localhost:${info.port}`)
})

```

### packages/shared/src/index.ts

```typescript
// Shared API contracts imported by both the web frontend and the Hono server.
// Define request/response shapes here once so both ends stay in sync.

export interface HealthResponse {
  status: 'ok'
  time: string
}

export type VidscribeNoteKind = 'text' | 'voice' | 'visual' | 'browserbase'

export type VidscribeNote = {
  id: string
  videoId: string
  timestampSec: number
  kind: VidscribeNoteKind
  text: string
  title?: string
  parentNoteId?: string
  imageDataUrl?: string
  transcriptContext?: string
  aiExplanation?: string
  researchKeywords?: string[]
  researchSummary?: string
  researchLinks?: string[]
  browserbaseMode?: 'beginner' | 'advanced'
  sources?: {
    title: string
    url: string
    summary: string
  }[]
  createdAt: string
}

// --- Videos -------------------------------------------------------------------
// Videos are stored in MongoDB via GridFS (the file bytes) plus a small amount
// of metadata (title, thumbnail) on the GridFS file document. The dashboard
// lists videos using only their metadata + thumbnail — never the file bytes.

/** A video as shown on the dashboard — metadata + thumbnail only, no bytes. */
export interface VideoSummary {
  /** GridFS file id, as a hex string. Also used as the note `videoId`. */
  id: string
  /** Human-readable title (defaults to the uploaded file name). */
  title: string
  /** A small still frame captured at upload time, as a data URL (or null). */
  thumbnailDataUrl: string | null
  /** MIME type of the stored video (e.g. video/mp4). */
  contentType: string
  /** Size of the stored video in bytes. */
  sizeBytes: number
  /** Duration in seconds, if it could be determined at upload. */
  durationSec?: number
  /** ISO timestamp of when the video was uploaded. */
  createdAt: string
}

/** A single video plus the notes taken against it (pulled together). */
export interface VideoDetail extends VideoSummary {
  notes: VidscribeNote[]
}

/** Response from GET /api/videos. */
export interface VideoListResponse {
  videos: VideoSummary[]
}

// --- Transcripts ----------------------------------------------------------
// Each uploaded video can be transcribed once via Deepgram. The result is
// stored as timestamped segments (Deepgram "utterances" — sentence-like
// chunks, not individual words) so later work (a transcript viewer, Redis
// RAG ingest) can use them without re-calling Deepgram.

/** One timestamped chunk of a video's transcript. */
export interface TranscriptSegment {
  startSec: number
  endSec: number
  text: string
}

/** A video's full transcript, stored once per video. */
export interface VideoTranscript {
  /** Same id as the video (GridFS file id, hex string). */
  videoId: string
  segments: TranscriptSegment[]
  /** Deepgram model used (or "mock" when MOCK_DEEPGRAM is on), for debugging. */
  model: string
  createdAt: string
}

// --- Study chatbot ------------------------------------------------------------
// The chatbot answers questions across the student's videos using vector search
// over their notes / research / lens explanations, grounded with Claude. It
// returns the video(s) the answer drew from so the UI can link to them.

/** One video an answer was drawn from. */
export interface ChatSource {
  video_id: string
  video_title: string
  /**
   * When the answer drew on a timestamped chunk (a transcript segment, or a note
   * taken at a moment), the point in the video to jump to, in seconds.
   */
  timestamp_sec?: number
}

/** Request body for POST /api/chat. */
export interface ChatRequest {
  message: string
  /** Opaque per-conversation id used to thread follow-up questions. */
  session_id?: string
  /** When set, scope retrieval to a single video (e.g. asking from its page). */
  video_id?: string
}

/** Response from POST /api/chat. */
export interface ChatResponse {
  answer: string
  /** Deduped videos the answer came from; empty when nothing relevant matched. */
  sources: ChatSource[]
}

/** Response from GET /api/videos/:id/transcript/window — segments around a timestamp. */
export interface TranscriptWindow {
  videoId: string
  timestamp: number
  radius: number
  windowStartSec: number
  windowEndSec: number
  segments: TranscriptSegment[]
  /** The overlapping segments' text, joined with spaces, ready to drop into a prompt. */
  context: string
}

// --- Study guide --------------------------------------------------------------
// Generated on demand from a video's (or the whole library's) notes, lens
// explanations, research summaries, and transcript. Not persisted — regenerated
// each request.

/** One themed section of a study guide. */
export interface StudyGuideSection {
  heading: string
  points: string[]
}

/** A generated study guide. */
export interface StudyGuide {
  title: string
  overview: string
  sections: StudyGuideSection[]
  /** Present when generated for a single video. */
  videoId?: string
}

/** Request body for POST /api/study-guide. */
export interface StudyGuideRequest {
  /** Omit to build from the whole library; set to scope to one video. */
  video_id?: string
}

/** Response from POST /api/study-guide. */
export interface StudyGuideResponse {
  guide: StudyGuide
}

// --- Flashcards (SM-2 spaced repetition) --------------------------------------
// Generated from the same material as the study guide, then persisted so review
// scheduling (SM-2) survives across sessions.

/** Anki-style review grades, mapped to SM-2 quality internally. */
export type FlashcardGrade = 'again' | 'hard' | 'good' | 'easy'

/** A single flashcard plus its SM-2 scheduling state. */
export interface Flashcard {
  id: string
  /** Which video the card came from; absent for whole-library decks. */
  videoId?: string
  front: string
  back: string
  /** SM-2 ease factor (>= 1.3), starts at 2.5. */
  ease: number
  /** Current inter-review interval in days. */
  intervalDays: number
  /** Consecutive successful reviews. */
  repetitions: number
  /** ISO timest
[truncated — 1558 more characters]
```

[51 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]