eshameo045 commited on
Commit
66d10f7
·
1 Parent(s): 8f9825f

Updated - mobile responsive + educational filter

Browse files
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .env
2
+ .vscode
3
+ __pycache__/
4
+ *.pyc
5
+ venv/
6
+ *.pyc
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements first (Docker cache optimization)
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Pre-download the embedding model so it's baked into the image
15
+ RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')"
16
+
17
+ # Copy app files
18
+ COPY . .
19
+
20
+ # Expose port (HuggingFace uses 7860)
21
+ EXPOSE 7860
22
+
23
+ # Set environment variables
24
+ ENV PYTHONUNBUFFERED=1
25
+ ENV FLASK_ENV=production
26
+
27
+ # Run with gunicorn
28
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--timeout", "120", "app:app"]
README.md DELETED
@@ -1,12 +0,0 @@
1
- ---
2
- title: LectureLens AI
3
- emoji: 🐨
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: 'Intelligent YouTube lecture assistant '
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
SETUP_GUIDE.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔬 LectureLens AI — Complete Setup Guide
2
+
3
+ ## 📁 Project Structure
4
+ lecturelens/
5
+ ├── app.py # Main Flask app + all API routes
6
+ ├── requirements.txt # All Python dependencies
7
+ ├── Dockerfile # HuggingFace deployment config
8
+ ├── .env # API keys (NEVER upload this!)
9
+ ├── .gitignore # Tells git to ignore .env
10
+ ├── README.md # Project documentation
11
+ ├── SETUP_GUIDE.md # This file
12
+ ├── templates/
13
+ │ └── index.html # Complete frontend UI
14
+ └── utils/
15
+ ├── init.py # Package init
16
+ ├── transcript_handler.py # YouTube transcript + title extraction
17
+ ├── embedder.py # TF-IDF search engine
18
+ └── llm_handler.py # GPT-4o-mini — all AI features
19
+
20
+ ---
21
+
22
+ ## 🔑 API Keys Required
23
+
24
+ ### OpenAI API Key (Only One Required!)
25
+ 1. Go to: https://platform.openai.com
26
+ 2. Sign up / Login
27
+ 3. Click "API Keys" → "Create new secret key"
28
+ 4. Copy your key (starts with `sk-`)
29
+
30
+ ---
31
+
32
+ ## 🖥️ Local Setup
33
+
34
+ ### Step 1: Install Python
35
+ Download from: https://python.org (Python 3.10+)
36
+
37
+ ### Step 2: Download Project
38
+ Download and extract the project folder
39
+
40
+ ### Step 3: Create Virtual Environment
41
+ ```bash
42
+ # Windows
43
+ python -m venv venv
44
+ venv\Scripts\activate
45
+
46
+ # Mac/Linux
47
+ python3 -m venv venv
48
+ source venv/bin/activate
49
+ ```
50
+
51
+ ### Step 4: Install Dependencies
52
+ ```bash
53
+ pip install -r requirements.txt
54
+ ```
55
+
56
+ ### Step 5: Create `.env` File
57
+ Create a file named `.env` in the project root:
58
+ OPENAI_API_KEY=sk-your-key-here
59
+ SECRET_KEY=lecturelens-secret-2024
60
+
61
+ ### Step 6: Create `.gitignore` File
62
+ .env
63
+ pycache/
64
+ *.pyc
65
+ venv/
66
+
67
+ ### Step 7: Run the App
68
+ ```bash
69
+ python app.py
70
+ ```
71
+
72
+ ### Step 8: Open Browser
73
+ http://localhost:7860
74
+
75
+ ---
76
+
77
+ ## 🎯 How to Use
78
+
79
+ ### 1. Process a Video
80
+ - Paste any YouTube lecture URL
81
+ - Click "Analyze Video"
82
+ - Wait for transcript extraction
83
+
84
+ ### 2. Generate Study Material
85
+ - Click **Summary** → Generate ✨ → Get comprehensive summary
86
+ - Click **Flashcards** → Generate ✨ → Get 10 Q&A cards
87
+ - Click **Sticky Notes** → Generate ✨ → Get 8 key point notes
88
+ - Click **Flowchart** → Generate ✨ → Get concept map
89
+ - Click **Quiz** → Generate ✨ → Get 5 MCQ questions
90
+
91
+ ### 3. Chat with AI
92
+ - Type any question about the lecture
93
+ - AI answers strictly based on video content
94
+ - Previous questions are remembered in conversation
95
+
96
+ ### 4. Export Content
97
+ - Click 📄 PDF button on any panel to download
98
+ - Click 📋 Copy button to copy to clipboard
99
+
100
+ ### 5. Compare Videos
101
+ - Go to Compare tab
102
+ - Enter second YouTube URL
103
+ - Click Compare ✨
104
+
105
+ ### 6. Change Language
106
+ - Click English / اردو / Roman Urdu buttons at top
107
+ - All generated content will be in selected language
108
+
109
+ ---
110
+
111
+ ## 🌐 How It Works — Technical Flow
112
+ User enters YouTube URL
113
+
114
+ youtube-transcript-api → extracts captions (free)
115
+
116
+ yt-dlp → fetches video title (free)
117
+
118
+ TF-IDF (scikit-learn) → splits into chunks, builds search index (free, local)
119
+
120
+ User asks question / clicks Generate
121
+
122
+ TF-IDF → finds most relevant transcript chunks
123
+
124
+ GPT-4o-mini → generates answer from chunks (paid, ~$0.01/session)
125
+
126
+ Response shown in UI
127
+
128
+ ---
129
+
130
+ ## 📚 Libraries Explained
131
+
132
+ | Library | Purpose | Cost |
133
+ |---------|---------|------|
134
+ | `flask` | Web server + API endpoints | FREE |
135
+ | `youtube-transcript-api` | Extract YouTube captions | FREE |
136
+ | `yt-dlp` | Fetch video title | FREE |
137
+ | `scikit-learn` | TF-IDF text search | FREE |
138
+ | `openai` | GPT-4o-mini AI responses | ~$0.01/session |
139
+ | `reportlab` | PDF generation | FREE |
140
+ | `python-dotenv` | Load .env API keys | FREE |
141
+ | `gunicorn` | Production server | FREE |
142
+
143
+ ---
144
+
145
+ ## 🚀 Deploy to HuggingFace Spaces (FREE Hosting)
146
+
147
+ ### Step 1: Create HuggingFace Account
148
+ Go to: https://huggingface.co — sign up free
149
+
150
+ ### Step 2: Create New Space
151
+ 1. Click profile → "New Space"
152
+ 2. Name: `lecturelens-ai`
153
+ 3. SDK: **Docker**
154
+ 4. Visibility: Public (free)
155
+ 5. Click "Create Space"
156
+
157
+ ### Step 3: Upload Files
158
+ Upload all project files EXCEPT `.env` file
159
+
160
+ ### Step 4: Add Secret Key
161
+ 1. Go to Space → Settings → Repository Secrets
162
+ 2. Add:
163
+ - Name: `OPENAI_API_KEY`
164
+ - Value: `sk-your-key-here`
165
+
166
+ ### Step 5: Deploy
167
+ HuggingFace automatically builds and deploys!
168
+
169
+ Your app: `https://huggingface.co/spaces/YOUR_USERNAME/lecturelens-ai`
170
+
171
+ ---
172
+
173
+ ## ❌ Common Errors & Fixes
174
+
175
+ | Error | Fix |
176
+ |-------|-----|
177
+ | `No transcript found` | Video does not have captions enabled |
178
+ | `OPENAI_API_KEY not set` | Check .env file |
179
+ | `Module not found` | Run `pip install -r requirements.txt` |
180
+ | `Port already in use` | Change port in app.py or kill existing process |
181
+ | `Invalid YouTube URL` | Make sure URL has `watch?v=` or `youtu.be/` |
182
+
183
+ ---
184
+
185
+ ## ⚠️ Important Notes
186
+
187
+ - Never upload `.env` file to GitHub or HuggingFace
188
+ - Videos must have captions/subtitles enabled
189
+ - Session data resets when server restarts
190
+ - Supports English, Hindi, Urdu transcripts
191
+ - AI answers strictly based on video content only
192
+
193
+ ---
194
+
195
+ ## 💰 Cost
196
+
197
+ | Component | Cost |
198
+ |-----------|------|
199
+ | Everything except OpenAI | FREE |
200
+ | OpenAI GPT-4o-mini | ~$0.01 per lecture session |
201
+ | HuggingFace hosting | FREE |
202
+ | **Total per session** | **~$0.01** |
203
+
204
+ ---
205
+
app.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, render_template, session
2
+ from utils.transcript_handler import get_transcript
3
+ from utils.embedder import EmbeddingHandler
4
+ from utils.llm_handler import LLMHandler
5
+ import os
6
+ from reportlab.lib.pagesizes import letter
7
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
8
+ from reportlab.lib.styles import getSampleStyleSheet
9
+ from io import BytesIO
10
+ from flask import send_file
11
+ import uuid
12
+ from dotenv import load_dotenv # ← Line 1 add karo
13
+ load_dotenv() # ← Line 2 add karo
14
+
15
+ from flask import Flask, request, jsonify, render_template, session
16
+ # ... baaki code same rehega
17
+
18
+ app = Flask(__name__)
19
+ app.secret_key = os.environ.get("SECRET_KEY", "lecturelens-secret-2024")
20
+
21
+ embedding_handler = EmbeddingHandler()
22
+ llm_handler = LLMHandler()
23
+
24
+ # In-memory session store (use Redis in production)
25
+ sessions = {}
26
+
27
+ @app.route("/")
28
+ def index():
29
+ return render_template("index.html")
30
+
31
+ @app.route("/api/process", methods=["POST"])
32
+ def process_video():
33
+ data = request.get_json()
34
+ url = data.get("url", "").strip()
35
+
36
+ if not url:
37
+ return jsonify({"error": "YouTube URL required"}), 400
38
+
39
+ try:
40
+ # Extract transcript
41
+ transcript_data = get_transcript(url)
42
+ if not transcript_data["success"]:
43
+ return jsonify({"error": transcript_data["error"]}), 400
44
+
45
+ transcript_text = transcript_data["transcript"]
46
+ video_title = transcript_data.get("title", "YouTube Video")
47
+
48
+ # Create embeddings and store
49
+ session_id = str(uuid.uuid4())
50
+ is_educational = llm_handler.check_educational(transcript_text, video_title)
51
+ if not is_educational:
52
+ return jsonify({
53
+ "error": "⚠️ This video does not appear to be an educational lecture. Please provide a study or lecture video!"
54
+ }), 400
55
+ embedding_handler.process_and_store(transcript_text, session_id)
56
+
57
+ # Store session info
58
+ sessions[session_id] = {
59
+ "transcript": transcript_text,
60
+ "title": video_title,
61
+ "url": url,
62
+ "messages": [] # ✅ Chat history yahan save hogi
63
+ }
64
+
65
+
66
+ return jsonify({
67
+ "success": True,
68
+ "session_id": session_id,
69
+ "title": video_title,
70
+ "transcript": transcript_text,
71
+ "transcript_length": len(transcript_text),
72
+ # "video_id": video_id,
73
+ "message": "Video processed successfully!"
74
+ })
75
+
76
+
77
+ except Exception as e:
78
+ return jsonify({"error": str(e)}), 500
79
+
80
+
81
+ @app.route("/api/chat", methods=["POST"])
82
+ def chat():
83
+ data = request.get_json()
84
+ session_id = data.get("session_id")
85
+ question = data.get("question", "").strip()
86
+ language = data.get("language", "english")
87
+
88
+ if not session_id or session_id not in sessions:
89
+ return jsonify({"error": "Invalid session. Please process a video first."}), 400
90
+
91
+ if not question:
92
+ return jsonify({"error": "Question cannot be empty"}), 400
93
+
94
+ try:
95
+ # Retrieve relevant chunks
96
+ relevant_chunks = embedding_handler.retrieve(question, session_id)
97
+
98
+ # History mein question add karo
99
+ sessions[session_id]["messages"].append({
100
+ "role": "user",
101
+ "content": question
102
+ })
103
+
104
+ # Generate answer
105
+ answer = llm_handler.answer_question(
106
+ question=question,
107
+ context=relevant_chunks,
108
+ language=language,
109
+ video_title=sessions[session_id]["title"],
110
+ history=sessions[session_id]["messages"]
111
+ )
112
+
113
+ # History mein answer save karo
114
+ sessions[session_id]["messages"].append({
115
+ "role": "assistant",
116
+ "content": answer
117
+ })
118
+
119
+ return jsonify({"success": True, "answer": answer})
120
+
121
+ except Exception as e:
122
+ return jsonify({"error": str(e)}), 500
123
+
124
+ @app.route("/api/summarize", methods=["POST"])
125
+ def summarize():
126
+ data = request.get_json()
127
+ session_id = data.get("session_id")
128
+ language = data.get("language", "english")
129
+
130
+ if not session_id or session_id not in sessions:
131
+ return jsonify({"error": "Invalid session"}), 400
132
+
133
+ try:
134
+ transcript = sessions[session_id]["transcript"]
135
+ title = sessions[session_id]["title"]
136
+ summary = llm_handler.summarize(transcript, language, title)
137
+ return jsonify({"success": True, "summary": summary})
138
+ except Exception as e:
139
+ return jsonify({"error": str(e)}), 500
140
+
141
+
142
+ @app.route("/api/flashcards", methods=["POST"])
143
+ def flashcards():
144
+ data = request.get_json()
145
+ session_id = data.get("session_id")
146
+ language = data.get("language", "english")
147
+
148
+ if not session_id or session_id not in sessions:
149
+ return jsonify({"error": "Invalid session"}), 400
150
+
151
+ try:
152
+ transcript = sessions[session_id]["transcript"]
153
+ title = sessions[session_id]["title"]
154
+ cards = llm_handler.generate_flashcards(transcript, language, title)
155
+ return jsonify({"success": True, "flashcards": cards})
156
+ except Exception as e:
157
+ return jsonify({"error": str(e)}), 500
158
+
159
+
160
+ @app.route("/api/notes", methods=["POST"])
161
+ def sticky_notes():
162
+ data = request.get_json()
163
+ session_id = data.get("session_id")
164
+ language = data.get("language", "english")
165
+
166
+ if not session_id or session_id not in sessions:
167
+ return jsonify({"error": "Invalid session"}), 400
168
+
169
+ try:
170
+ transcript = sessions[session_id]["transcript"]
171
+ title = sessions[session_id]["title"]
172
+ notes = llm_handler.generate_notes(transcript, language, title)
173
+ return jsonify({"success": True, "notes": notes})
174
+ except Exception as e:
175
+ return jsonify({"error": str(e)}), 500
176
+
177
+
178
+ @app.route("/api/flowchart", methods=["POST"])
179
+ def flowchart():
180
+ data = request.get_json()
181
+ session_id = data.get("session_id")
182
+ language = data.get("language", "english")
183
+
184
+ if not session_id or session_id not in sessions:
185
+ return jsonify({"error": "Invalid session"}), 400
186
+
187
+ try:
188
+ transcript = sessions[session_id]["transcript"]
189
+ title = sessions[session_id]["title"]
190
+ flowchart_data = llm_handler.generate_flowchart(transcript, language, title)
191
+ return jsonify({"success": True, "flowchart": flowchart_data})
192
+ except Exception as e:
193
+ return jsonify({"error": str(e)}), 500
194
+
195
+
196
+
197
+
198
+ @app.route("/api/quiz", methods=["POST"])
199
+ def quiz():
200
+ data = request.get_json()
201
+ session_id = data.get("session_id")
202
+ language = data.get("language", "english")
203
+ if not session_id or session_id not in sessions:
204
+ return jsonify({"error": "Invalid session"}), 400
205
+ try:
206
+ transcript = sessions[session_id]["transcript"]
207
+ title = sessions[session_id]["title"]
208
+ questions = llm_handler.generate_quiz(transcript, language, title)
209
+ if not questions:
210
+ return jsonify({"error": "Could not generate quiz. Try again!"}), 400
211
+ return jsonify({"success": True, "questions": questions})
212
+
213
+ except Exception as e:
214
+ print(f"QUIZ ROUTE ERROR: {str(e)}")
215
+ return jsonify({"error": str(e)}), 500
216
+
217
+ @app.route("/api/compare", methods=["POST"])
218
+ def compare():
219
+ data = request.get_json()
220
+ session_id = data.get("session_id")
221
+ url2 = data.get("url2", "").strip()
222
+ language = data.get("language", "english")
223
+
224
+ if not session_id or session_id not in sessions:
225
+ return jsonify({"error": "Invalid session"}), 400
226
+ if not url2:
227
+ return jsonify({"error": "Second URL required"}), 400
228
+
229
+ try:
230
+ transcript2 = get_transcript(url2)
231
+ if not transcript2["success"]:
232
+ return jsonify({"error": transcript2["error"]}), 400
233
+
234
+ transcript1 = sessions[session_id]["transcript"]
235
+ title1 = sessions[session_id]["title"]
236
+ title2 = transcript2["title"]
237
+
238
+ result = llm_handler.compare_videos(
239
+ transcript1, title1,
240
+ transcript2["transcript"], title2,
241
+ language
242
+ )
243
+ return jsonify({"success": True, "comparison": result})
244
+ except Exception as e:
245
+ return jsonify({"error": str(e)}), 500
246
+
247
+
248
+ @app.route("/api/export", methods=["POST"])
249
+ def export_pdf():
250
+ data = request.get_json()
251
+ session_id = data.get("session_id")
252
+ content = data.get("content", "")
253
+ title = data.get("title", "LectureLens Export")
254
+
255
+ if not session_id or session_id not in sessions:
256
+ return jsonify({"error": "Invalid session"}), 400
257
+
258
+ try:
259
+ buffer = BytesIO()
260
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
261
+ styles = getSampleStyleSheet()
262
+ story = []
263
+
264
+ story.append(Paragraph(title, styles['Title']))
265
+ story.append(Spacer(1, 20))
266
+
267
+ for line in content.split('\n'):
268
+ if line.strip():
269
+ story.append(Paragraph(line, styles['Normal']))
270
+ story.append(Spacer(1, 6))
271
+
272
+ doc.build(story)
273
+ buffer.seek(0)
274
+
275
+ return send_file(
276
+ buffer,
277
+ as_attachment=True,
278
+ download_name="lecturelens_export.pdf",
279
+ mimetype='application/pdf'
280
+ )
281
+ except Exception as e:
282
+ return jsonify({"error": str(e)}), 500
283
+
284
+
285
+ if __name__ == "__main__":
286
+ app.run(debug=True, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ flask==3.0.3
2
+ youtube-transcript-api==0.6.2
3
+ scikit-learn==1.4.2
4
+ openai==1.30.0
5
+ python-dotenv==1.0.1
6
+ yt-dlp==2024.8.6
7
+ reportlab==4.2.0
8
+ gunicorn==22.0.0
9
+ flask-limiter==3.7.0
templates/index.html ADDED
@@ -0,0 +1,1422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LectureLens AI — Study Smarter</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #0a0a0f;
11
+ --surface: #12121a;
12
+ --surface2: #1a1a26;
13
+ --border: #2a2a3d;
14
+ --accent: #7c6fff;
15
+ --accent2: #ff6b9d;
16
+ --accent3: #6bffb8;
17
+ --text: #e8e8f0;
18
+ --text-dim: #8888aa;
19
+ --yellow: #ffd166;
20
+ --blue: #118ab2;
21
+ --green: #06d6a0;
22
+ --pink: #ef476f;
23
+ --purple: #9b5de5;
24
+ --glow: rgba(124, 111, 255, 0.15);
25
+ }
26
+
27
+ * { margin: 0; padding: 0; box-sizing: border-box; }
28
+
29
+ body {
30
+ background: var(--bg);
31
+ color: var(--text);
32
+ font-family: 'DM Sans', sans-serif;
33
+ min-height: 100vh;
34
+ overflow-x: hidden;
35
+ }
36
+
37
+ /* Animated background */
38
+ body::before {
39
+ content: '';
40
+ position: fixed;
41
+ top: -50%;
42
+ left: -50%;
43
+ width: 200%;
44
+ height: 200%;
45
+ background:
46
+ radial-gradient(ellipse at 20% 20%, rgba(124, 111, 255, 0.08) 0%, transparent 50%),
47
+ radial-gradient(ellipse at 80% 80%, rgba(255, 107, 157, 0.06) 0%, transparent 50%),
48
+ radial-gradient(ellipse at 50% 50%, rgba(107, 255, 184, 0.04) 0%, transparent 60%);
49
+ animation: bgShift 15s ease-in-out infinite alternate;
50
+ pointer-events: none;
51
+ z-index: 0;
52
+ }
53
+
54
+ @keyframes bgShift {
55
+ 0% { transform: translate(0, 0) rotate(0deg); }
56
+ 100% { transform: translate(-30px, -20px) rotate(1deg); }
57
+ }
58
+
59
+ .app-container {
60
+ position: relative;
61
+ z-index: 1;
62
+ max-width: 1300px;
63
+ margin: 0 auto;
64
+ padding: 0 24px;
65
+ }
66
+
67
+ /* ===== HEADER ===== */
68
+ header {
69
+ padding: 28px 0 20px;
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: space-between;
73
+ border-bottom: 1px solid var(--border);
74
+ margin-bottom: 40px;
75
+ }
76
+
77
+ .logo {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 12px;
81
+ }
82
+
83
+ .logo-icon {
84
+ width: 42px;
85
+ height: 42px;
86
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
87
+ border-radius: 12px;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ font-size: 20px;
92
+ box-shadow: 0 0 20px rgba(124, 111, 255, 0.3);
93
+ }
94
+
95
+ .logo-text {
96
+ font-family: 'Syne', sans-serif;
97
+ font-size: 22px;
98
+ font-weight: 800;
99
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
100
+ -webkit-background-clip: text;
101
+ -webkit-text-fill-color: transparent;
102
+ background-clip: text;
103
+ }
104
+
105
+ .logo-sub {
106
+ font-size: 11px;
107
+ color: var(--text-dim);
108
+ font-weight: 300;
109
+ letter-spacing: 2px;
110
+ text-transform: uppercase;
111
+ margin-top: 2px;
112
+ }
113
+
114
+ .lang-selector {
115
+ display: flex;
116
+ gap: 8px;
117
+ background: var(--surface);
118
+ padding: 6px;
119
+ border-radius: 12px;
120
+ border: 1px solid var(--border);
121
+ }
122
+
123
+ .lang-btn {
124
+ padding: 6px 16px;
125
+ border: none;
126
+ border-radius: 8px;
127
+ cursor: pointer;
128
+ font-family: 'DM Sans', sans-serif;
129
+ font-size: 13px;
130
+ font-weight: 500;
131
+ transition: all 0.2s;
132
+ background: transparent;
133
+ color: var(--text-dim);
134
+ }
135
+
136
+ .lang-btn.active {
137
+ background: var(--accent);
138
+ color: white;
139
+ box-shadow: 0 0 12px rgba(124, 111, 255, 0.4);
140
+ }
141
+
142
+ /* ===== URL INPUT SECTION ===== */
143
+ .url-section {
144
+ background: var(--surface);
145
+ border: 1px solid var(--border);
146
+ border-radius: 20px;
147
+ padding: 32px;
148
+ margin-bottom: 32px;
149
+ transition: border-color 0.3s;
150
+ }
151
+
152
+ .url-section:hover {
153
+ border-color: var(--accent);
154
+ }
155
+
156
+ .section-label {
157
+ font-family: 'Syne', sans-serif;
158
+ font-size: 13px;
159
+ font-weight: 600;
160
+ color: var(--accent);
161
+ letter-spacing: 2px;
162
+ text-transform: uppercase;
163
+ margin-bottom: 16px;
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ }
168
+
169
+ .url-input-row {
170
+ display: flex;
171
+ gap: 12px;
172
+ }
173
+
174
+ .url-input {
175
+ flex: 1;
176
+ background: var(--bg);
177
+ border: 1px solid var(--border);
178
+ border-radius: 12px;
179
+ padding: 14px 20px;
180
+ color: var(--text);
181
+ font-family: 'DM Sans', sans-serif;
182
+ font-size: 15px;
183
+ transition: all 0.3s;
184
+ outline: none;
185
+ }
186
+
187
+ .url-input:focus {
188
+ border-color: var(--accent);
189
+ box-shadow: 0 0 0 3px rgba(124, 111, 255, 0.15);
190
+ }
191
+
192
+ .url-input::placeholder { color: var(--text-dim); }
193
+
194
+ .process-btn {
195
+ background: linear-gradient(135deg, var(--accent), #9b8fff);
196
+ border: none;
197
+ border-radius: 12px;
198
+ padding: 14px 28px;
199
+ color: white;
200
+ font-family: 'Syne', sans-serif;
201
+ font-size: 14px;
202
+ font-weight: 700;
203
+ cursor: pointer;
204
+ transition: all 0.3s;
205
+ letter-spacing: 0.5px;
206
+ white-space: nowrap;
207
+ position: relative;
208
+ overflow: hidden;
209
+ }
210
+
211
+ .process-btn::after {
212
+ content: '';
213
+ position: absolute;
214
+ top: 0; left: -100%;
215
+ width: 100%; height: 100%;
216
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
217
+ transition: 0.5s;
218
+ }
219
+
220
+ .process-btn:hover::after { left: 100%; }
221
+ .process-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(124, 111, 255, 0.4); }
222
+ .process-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
223
+
224
+ /* ===== STATUS BAR ===== */
225
+ .status-bar {
226
+ display: none;
227
+ align-items: center;
228
+ gap: 10px;
229
+ margin-top: 16px;
230
+ padding: 12px 16px;
231
+ border-radius: 10px;
232
+ font-size: 14px;
233
+ }
234
+
235
+ .status-bar.loading {
236
+ display: flex;
237
+ background: rgba(124, 111, 255, 0.1);
238
+ border: 1px solid rgba(124, 111, 255, 0.3);
239
+ color: var(--accent);
240
+ }
241
+
242
+ .status-bar.success {
243
+ display: flex;
244
+ background: rgba(107, 255, 184, 0.1);
245
+ border: 1px solid rgba(107, 255, 184, 0.3);
246
+ color: var(--accent3);
247
+ }
248
+
249
+ .status-bar.error {
250
+ display: flex;
251
+ background: rgba(239, 71, 111, 0.1);
252
+ border: 1px solid rgba(239, 71, 111, 0.3);
253
+ color: var(--pink);
254
+ }
255
+
256
+ .spinner {
257
+ width: 16px; height: 16px;
258
+ border: 2px solid rgba(124, 111, 255, 0.3);
259
+ border-top-color: var(--accent);
260
+ border-radius: 50%;
261
+ animation: spin 0.8s linear infinite;
262
+ }
263
+
264
+ @keyframes spin { to { transform: rotate(360deg); } }
265
+
266
+ /* ===== MAIN GRID ===== */
267
+ .main-grid {
268
+ display: none;
269
+ grid-template-columns: 1fr 380px;
270
+ gap: 24px;
271
+ margin-bottom: 40px;
272
+ }
273
+
274
+ .main-grid.visible { display: grid; }
275
+
276
+ /* ===== TOOLS TABS ===== */
277
+ .tools-panel {
278
+ display: flex;
279
+ flex-direction: column;
280
+ gap: 20px;
281
+ }
282
+
283
+ .tabs-row {
284
+ display: flex;
285
+ gap: 8px;
286
+ flex-wrap: wrap;
287
+ }
288
+
289
+ .tab-btn {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 6px;
293
+ padding: 10px 18px;
294
+ border: 1px solid var(--border);
295
+ border-radius: 10px;
296
+ background: var(--surface);
297
+ color: var(--text-dim);
298
+ font-family: 'DM Sans', sans-serif;
299
+ font-size: 13px;
300
+ font-weight: 500;
301
+ cursor: pointer;
302
+ transition: all 0.25s;
303
+ }
304
+
305
+ .tab-btn:hover {
306
+ border-color: var(--accent);
307
+ color: var(--text);
308
+ }
309
+
310
+ .tab-btn.active {
311
+ background: var(--accent);
312
+ border-color: var(--accent);
313
+ color: white;
314
+ box-shadow: 0 4px 15px rgba(124, 111, 255, 0.3);
315
+ }
316
+
317
+ /* ===== CONTENT PANELS ===== */
318
+ .content-panel {
319
+ display: none;
320
+ background: var(--surface);
321
+ border: 1px solid var(--border);
322
+ border-radius: 20px;
323
+ padding: 28px;
324
+ min-height: 400px;
325
+ flex-direction: column;
326
+ gap: 16px;
327
+ }
328
+
329
+ .content-panel.active { display: flex; }
330
+
331
+ .panel-header {
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: space-between;
335
+ margin-bottom: 8px;
336
+ }
337
+
338
+ .panel-title {
339
+ font-family: 'Syne', sans-serif;
340
+ font-size: 18px;
341
+ font-weight: 700;
342
+ color: var(--text);
343
+ }
344
+
345
+ .generate-btn {
346
+ background: linear-gradient(135deg, var(--accent2), #ff8fb8);
347
+ border: none;
348
+ border-radius: 8px;
349
+ padding: 8px 20px;
350
+ color: white;
351
+ font-family: 'DM Sans', sans-serif;
352
+ font-size: 13px;
353
+ font-weight: 500;
354
+ cursor: pointer;
355
+ transition: all 0.2s;
356
+ }
357
+
358
+ .generate-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(255, 107, 157, 0.3); }
359
+ .generate-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
360
+
361
+ .panel-content {
362
+ flex: 1;
363
+ overflow-y: auto;
364
+ color: var(--text);
365
+ line-height: 1.7;
366
+ font-size: 14px;
367
+ }
368
+
369
+ .panel-content::-webkit-scrollbar { width: 4px; }
370
+ .panel-content::-webkit-scrollbar-track { background: var(--surface2); }
371
+ .panel-content::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 2px; }
372
+
373
+ .empty-state {
374
+ display: flex;
375
+ flex-direction: column;
376
+ align-items: center;
377
+ justify-content: center;
378
+ height: 250px;
379
+ color: var(--text-dim);
380
+ gap: 12px;
381
+ text-align: center;
382
+ }
383
+
384
+ .empty-icon { font-size: 48px; opacity: 0.4; }
385
+
386
+ /* ===== SUMMARY CONTENT ===== */
387
+ .summary-text h1, .summary-text h2, .summary-text h3 {
388
+ font-family: 'Syne', sans-serif;
389
+ color: var(--accent);
390
+ margin: 16px 0 8px;
391
+ }
392
+ .summary-text h1 { font-size: 18px; }
393
+ .summary-text h2 { font-size: 16px; color: var(--accent2); }
394
+ .summary-text h3 { font-size: 14px; color: var(--accent3); }
395
+ .summary-text p { margin-bottom: 10px; }
396
+ .summary-text ul, .summary-text ol { padding-left: 20px; margin-bottom: 10px; }
397
+ .summary-text li { margin-bottom: 4px; }
398
+
399
+ /* ===== FLASHCARDS ===== */
400
+ .flashcards-grid {
401
+ display: flex;
402
+ flex-direction: column;
403
+ gap: 12px;
404
+ }
405
+
406
+ .flashcard {
407
+ background: var(--surface2);
408
+ border: 1px solid var(--border);
409
+ border-radius: 12px;
410
+ overflow: hidden;
411
+ cursor: pointer;
412
+ transition: all 0.3s;
413
+ }
414
+
415
+ .flashcard:hover { border-color: var(--accent); transform: translateX(4px); }
416
+
417
+ .card-question {
418
+ padding: 14px 18px;
419
+ font-weight: 500;
420
+ color: var(--text);
421
+ font-size: 14px;
422
+ display: flex;
423
+ align-items: flex-start;
424
+ gap: 10px;
425
+ }
426
+
427
+ .card-q-badge {
428
+ background: var(--accent);
429
+ color: white;
430
+ font-size: 10px;
431
+ font-weight: 700;
432
+ padding: 2px 6px;
433
+ border-radius: 4px;
434
+ white-space: nowrap;
435
+ margin-top: 2px;
436
+ font-family: 'Syne', sans-serif;
437
+ }
438
+
439
+ .card-answer {
440
+ display: none;
441
+ padding: 12px 18px;
442
+ border-top: 1px solid var(--border);
443
+ color: var(--text-dim);
444
+ font-size: 13px;
445
+ line-height: 1.6;
446
+ background: rgba(124, 111, 255, 0.05);
447
+ }
448
+
449
+ .flashcard.open .card-answer { display: block; }
450
+
451
+ .card-a-badge {
452
+ display: inline-block;
453
+ background: var(--accent3);
454
+ color: var(--bg);
455
+ font-size: 10px;
456
+ font-weight: 700;
457
+ padding: 2px 6px;
458
+ border-radius: 4px;
459
+ margin-bottom: 6px;
460
+ font-family: 'Syne', sans-serif;
461
+ }
462
+
463
+ /* ===== STICKY NOTES ===== */
464
+ .notes-grid {
465
+ display: grid;
466
+ grid-template-columns: 1fr 1fr;
467
+ gap: 12px;
468
+ }
469
+
470
+ .sticky-note {
471
+ border-radius: 12px;
472
+ padding: 16px;
473
+ position: relative;
474
+ transition: transform 0.2s;
475
+ }
476
+
477
+ .sticky-note:hover { transform: rotate(-1deg) scale(1.02); }
478
+
479
+ .sticky-note.yellow { background: rgba(255, 209, 102, 0.15); border: 1px solid rgba(255, 209, 102, 0.3); }
480
+ .sticky-note.blue { background: rgba(17, 138, 178, 0.15); border: 1px solid rgba(17, 138, 178, 0.3); }
481
+ .sticky-note.green { background: rgba(6, 214, 160, 0.15); border: 1px solid rgba(6, 214, 160, 0.3); }
482
+ .sticky-note.pink { background: rgba(239, 71, 111, 0.15); border: 1px solid rgba(239, 71, 111, 0.3); }
483
+ .sticky-note.purple { background: rgba(155, 93, 229, 0.15); border: 1px solid rgba(155, 93, 229, 0.3); }
484
+
485
+ .note-title {
486
+ font-family: 'Syne', sans-serif;
487
+ font-size: 13px;
488
+ font-weight: 700;
489
+ margin-bottom: 8px;
490
+ color: var(--text);
491
+ }
492
+
493
+ .note-content { font-size: 12px; color: var(--text-dim); line-height: 1.5; }
494
+
495
+ /* ===== FLOWCHART ===== */
496
+ .flowchart-container {
497
+ display: flex;
498
+ flex-direction: column;
499
+ align-items: center;
500
+ gap: 0;
501
+ padding: 16px;
502
+ }
503
+
504
+ .flow-node {
505
+ padding: 12px 24px;
506
+ border-radius: 10px;
507
+ font-size: 13px;
508
+ font-weight: 500;
509
+ text-align: center;
510
+ max-width: 280px;
511
+ width: 100%;
512
+ transition: transform 0.2s;
513
+ font-family: 'DM Sans', sans-serif;
514
+ }
515
+
516
+ .flow-node:hover { transform: scale(1.03); }
517
+
518
+ .flow-node.start { background: linear-gradient(135deg, var(--accent3), #00c984); color: #0a0a0f; font-weight: 700; border-radius: 50px; }
519
+ .flow-node.end { background: linear-gradient(135deg, var(--accent2), #ff4785); color: white; font-weight: 700; border-radius: 50px; }
520
+ .flow-node.process { background: var(--surface2); border: 1px solid var(--accent); color: var(--text); }
521
+ .flow-node.decision { background: rgba(255, 209, 102, 0.15); border: 1px solid var(--yellow); color: var(--yellow); transform: rotate(0deg); border-radius: 4px; }
522
+
523
+ .flow-arrow {
524
+ width: 2px;
525
+ height: 24px;
526
+ background: linear-gradient(to bottom, var(--accent), transparent);
527
+ position: relative;
528
+ margin: 0 auto;
529
+ }
530
+
531
+ .flow-arrow::after {
532
+ content: '▼';
533
+ position: absolute;
534
+ bottom: -8px;
535
+ left: 50%;
536
+ transform: translateX(-50%);
537
+ color: var(--accent);
538
+ font-size: 10px;
539
+ }
540
+
541
+ /* ===== CHAT PANEL ===== */
542
+ .chat-panel {
543
+ background: var(--surface);
544
+ border: 1px solid var(--border);
545
+ border-radius: 20px;
546
+ display: flex;
547
+ flex-direction: column;
548
+ height: fit-content;
549
+ position: sticky;
550
+ top: 24px;
551
+ }
552
+
553
+ .chat-header {
554
+ padding: 20px 24px;
555
+ border-bottom: 1px solid var(--border);
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 10px;
559
+ }
560
+
561
+ .chat-dot {
562
+ width: 8px; height: 8px;
563
+ border-radius: 50%;
564
+ background: var(--accent3);
565
+ box-shadow: 0 0 8px var(--accent3);
566
+ animation: pulse 2s infinite;
567
+ }
568
+
569
+ @keyframes pulse {
570
+ 0%, 100% { opacity: 1; }
571
+ 50% { opacity: 0.4; }
572
+ }
573
+
574
+ .chat-title {
575
+ font-family: 'Syne', sans-serif;
576
+ font-size: 15px;
577
+ font-weight: 700;
578
+ }
579
+
580
+ .chat-messages {
581
+ height: 380px;
582
+ overflow-y: auto;
583
+ padding: 16px;
584
+ display: flex;
585
+ flex-direction: column;
586
+ gap: 12px;
587
+ }
588
+
589
+ .chat-messages::-webkit-scrollbar { width: 4px; }
590
+ .chat-messages::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 2px; }
591
+
592
+ .msg {
593
+ max-width: 90%;
594
+ padding: 12px 16px;
595
+ border-radius: 14px;
596
+ font-size: 13px;
597
+ line-height: 1.6;
598
+ animation: msgIn 0.3s ease;
599
+ }
600
+
601
+ @keyframes msgIn {
602
+ from { opacity: 0; transform: translateY(8px); }
603
+ to { opacity: 1; transform: translateY(0); }
604
+ }
605
+
606
+ .msg.user {
607
+ align-self: flex-end;
608
+ background: linear-gradient(135deg, var(--accent), #9b8fff);
609
+ color: white;
610
+ border-bottom-right-radius: 4px;
611
+ }
612
+
613
+ .msg.ai {
614
+ align-self: flex-start;
615
+ background: var(--surface2);
616
+ color: var(--text);
617
+ border-bottom-left-radius: 4px;
618
+ border: 1px solid var(--border);
619
+ }
620
+
621
+ .msg.ai.typing {
622
+ display: flex;
623
+ gap: 4px;
624
+ align-items: center;
625
+ }
626
+
627
+ .typing-dot {
628
+ width: 6px; height: 6px;
629
+ border-radius: 50%;
630
+ background: var(--text-dim);
631
+ animation: typingBounce 1.2s infinite;
632
+ }
633
+
634
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
635
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
636
+
637
+ @keyframes typingBounce {
638
+ 0%, 60%, 100% { transform: translateY(0); }
639
+ 30% { transform: translateY(-6px); }
640
+ }
641
+
642
+ .chat-input-row {
643
+ padding: 16px;
644
+ border-top: 1px solid var(--border);
645
+ display: flex;
646
+ gap: 8px;
647
+ }
648
+
649
+ .chat-input {
650
+ flex: 1;
651
+ background: var(--surface2);
652
+ border: 1px solid var(--border);
653
+ border-radius: 10px;
654
+ padding: 10px 14px;
655
+ color: var(--text);
656
+ font-family: 'DM Sans', sans-serif;
657
+ font-size: 13px;
658
+ outline: none;
659
+ transition: border-color 0.2s;
660
+ resize: none;
661
+ }
662
+
663
+ .chat-input:focus { border-color: var(--accent); }
664
+ .chat-input::placeholder { color: var(--text-dim); }
665
+
666
+ .send-btn {
667
+ background: var(--accent);
668
+ border: none;
669
+ border-radius: 10px;
670
+ width: 40px;
671
+ height: 40px;
672
+ cursor: pointer;
673
+ display: flex;
674
+ align-items: center;
675
+ justify-content: center;
676
+ transition: all 0.2s;
677
+ flex-shrink: 0;
678
+ align-self: flex-end;
679
+ }
680
+
681
+ .send-btn:hover { background: #9b8fff; transform: scale(1.05); }
682
+ .send-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
683
+
684
+ /* ===== VIDEO INFO ===== */
685
+ .video-info-bar {
686
+ display: none;
687
+ background: linear-gradient(135deg, rgba(124, 111, 255, 0.1), rgba(255, 107, 157, 0.05));
688
+ border: 1px solid rgba(124, 111, 255, 0.2);
689
+ border-radius: 12px;
690
+ padding: 16px 20px;
691
+ margin-bottom: 24px;
692
+ align-items: center;
693
+ gap: 16px;
694
+ }
695
+
696
+ .video-info-bar.visible { display: flex; }
697
+
698
+ .video-thumb { font-size: 32px; }
699
+
700
+ .video-details h3 {
701
+ font-family: 'Syne', sans-serif;
702
+ font-size: 15px;
703
+ font-weight: 700;
704
+ margin-bottom: 4px;
705
+ }
706
+
707
+ .video-details p { font-size: 12px; color: var(--text-dim); }
708
+
709
+ /* ===== LOADING OVERLAY ===== */
710
+ .panel-loading {
711
+ display: none;
712
+ flex-direction: column;
713
+ align-items: center;
714
+ justify-content: center;
715
+ gap: 16px;
716
+ height: 200px;
717
+ color: var(--text-dim);
718
+ }
719
+
720
+ .panel-loading.visible { display: flex; }
721
+
722
+ .big-spinner {
723
+ width: 36px; height: 36px;
724
+ border: 3px solid rgba(124, 111, 255, 0.2);
725
+ border-top-color: var(--accent);
726
+ border-radius: 50%;
727
+ animation: spin 0.8s linear infinite;
728
+ }
729
+
730
+ /* Responsive */
731
+ @media (max-width: 900px) {
732
+ .main-grid { grid-template-columns: 1fr; }
733
+ .notes-grid { grid-template-columns: 1fr; }
734
+ .url-input-row { flex-direction: column; }
735
+ .chat-panel { position: relative; top: 0; }
736
+ .tabs-row { overflow-x: auto; flex-wrap: nowrap; padding-bottom: 8px; }
737
+ .tab-btn { white-space: nowrap; font-size: 12px; padding: 8px 12px; }
738
+ .lang-selector { flex-wrap: wrap; gap: 4px; }
739
+ .lang-btn { font-size: 11px; padding: 4px 10px; }
740
+ .logo-text { font-size: 18px; }
741
+ header { flex-wrap: wrap; gap: 12px; }
742
+ .panel-header { flex-wrap: wrap; gap: 8px; }
743
+ .chat-messages { height: 280px; }
744
+ .url-input { font-size: 14px; }
745
+ .process-btn { width: 100%; }
746
+ .video-info-bar { flex-wrap: wrap; }
747
+ .generate-btn { font-size: 11px; padding: 6px 12px; }
748
+ }
749
+
750
+ @media (max-width: 480px) {
751
+ .app-container { padding: 0 12px; }
752
+ .url-section { padding: 20px 16px; }
753
+ .content-panel { padding: 16px; }
754
+ .logo-sub { display: none; }
755
+ .flow-node { max-width: 100%; font-size: 12px; }
756
+ .notes-grid { grid-template-columns: 1fr; }
757
+ .panel-title { font-size: 15px; }
758
+ .chat-messages { height: 240px; }
759
+ .video-thumb img { width: 80px !important; }
760
+ header { padding: 16px 0; }
761
+ .url-section { padding: 16px; margin-bottom: 16px; }
762
+ }
763
+ </style>
764
+ </head>
765
+ <body>
766
+
767
+ <div class="app-container">
768
+ <!-- HEADER -->
769
+ <header>
770
+ <div class="logo">
771
+ <div class="logo-icon">🔬</div>
772
+ <div>
773
+ <div class="logo-text">LectureLens AI</div>
774
+ <div class="logo-sub">Study Smarter, Learn Deeper</div>
775
+ </div>
776
+ </div>
777
+ <div class="lang-selector">
778
+ <button class="lang-btn active" onclick="setLang('english', this)">English</button>
779
+ <button class="lang-btn" onclick="setLang('urdu', this)">اردو</button>
780
+ <button class="lang-btn" onclick="setLang('roman_urdu', this)">Roman Urdu</button>
781
+ </div>
782
+ </header>
783
+
784
+ <!-- URL INPUT -->
785
+ <div class="url-section">
786
+ <div class="section-label">📺 Step 1 — Enter YouTube Lecture URL</div>
787
+ <div class="url-input-row">
788
+ <input type="text" class="url-input" id="urlInput"
789
+ placeholder="https://www.youtube.com/watch?v=..."
790
+ onkeydown="if(event.key==='Enter') processVideo()">
791
+ <button class="process-btn" id="processBtn" onclick="processVideo()">
792
+ 🚀 Analyze Video
793
+ </button>
794
+ </div>
795
+ <div class="status-bar" id="statusBar">
796
+ <div class="spinner" id="statusSpinner"></div>
797
+ <span id="statusText">Processing video...</span>
798
+ </div>
799
+ </div>
800
+
801
+ <!-- VIDEO INFO -->
802
+ <div class="video-info-bar" id="videoInfoBar">
803
+ <div class="video-thumb">
804
+ <img id="videoThumbnail" src="" style="width:120px; border-radius:8px;" />
805
+ </div>
806
+ <div class="video-details">
807
+ <h3 id="videoTitle">Video Title</h3>
808
+ <p id="videoMeta">Transcript extracted and ready</p>
809
+ </div>
810
+ </div>
811
+
812
+ <!-- MAIN GRID -->
813
+ <div class="main-grid" id="mainGrid">
814
+ <!-- LEFT: TOOLS -->
815
+ <div class="tools-panel">
816
+ <!-- TABS -->
817
+ <div class="tabs-row">
818
+ <button class="tab-btn active" onclick="showTab('summary', this)">📋 Summary</button>
819
+ <button class="tab-btn" onclick="showTab('flashcards', this)">🃏 Flashcards</button>
820
+ <button class="tab-btn" onclick="showTab('notes', this)">📌 Sticky Notes</button>
821
+ <button class="tab-btn" onclick="showTab('flowchart', this)">🔄 Flowchart</button>
822
+ <button class="tab-btn" onclick="showTab('transcript', this)">📜 Transcript</button>
823
+ <button class="tab-btn" onclick="showTab('quiz', this)">🧠 Quiz</button>
824
+ <button class="tab-btn" onclick="showTab('compare', this)">🔀 Compare</button>
825
+ </div>
826
+
827
+ <!-- SUMMARY PANEL -->
828
+ <div class="content-panel active" id="panel-summary">
829
+ <div class="panel-header">
830
+ <div class="panel-title">📋 Lecture Summary</div>
831
+ <button class="generate-btn" id="summaryBtn" onclick="generateSummary()">Generate ✨</button>
832
+ <button class="generate-btn" onclick="copyContent('summaryContent')" style="background: linear-gradient(135deg, #118ab2, #06d6a0);">📋 Copy</button>
833
+ </div>
834
+ <div class="panel-content" id="summaryContent">
835
+ <div class="empty-state">
836
+ <div class="empty-icon">📋</div>
837
+ <div>Click "Generate" to create a comprehensive summary of the lecture.</div>
838
+ </div>
839
+ </div>
840
+ <button class="generate-btn" id="exportBtn"
841
+ onclick="exportPDF()"
842
+ style="background: linear-gradient(135deg, #06d6a0, #00c984); margin-top: 10px;">
843
+ 📄 Export PDF
844
+ </button>
845
+ </div>
846
+
847
+ <!-- FLASHCARDS PANEL -->
848
+ <div class="content-panel" id="panel-flashcards">
849
+ <div class="panel-header">
850
+ <div class="panel-title">🃏 Flashcards</div>
851
+ <button class="generate-btn" id="flashcardsBtn" onclick="generateFlashcards()">Generate ✨</button>
852
+ <button class="generate-btn" onclick="copyContent('flashcardsContent')" style="background: linear-gradient(135deg, #118ab2, #06d6a0);">📋 Copy</button>
853
+ <button class="generate-btn" onclick="exportSectionPDF('flashcardsContent')" style="background:linear-gradient(135deg, #06d6a0, #00c984);">📄 PDF</button>
854
+ </div>
855
+ <div class="panel-content" id="flashcardsContent">
856
+ <div class="empty-state">
857
+ <div class="empty-icon">🃏</div>
858
+ <div>Generate flashcards to test your knowledge. Click on a card to reveal the answer!</div>
859
+ </div>
860
+ </div>
861
+ </div>
862
+
863
+ <!-- NOTES PANEL -->
864
+ <div class="content-panel" id="panel-notes">
865
+ <div class="panel-header">
866
+ <div class="panel-title">📌 Sticky Notes</div>
867
+ <button class="generate-btn" id="notesBtn" onclick="generateNotes()">Generate ✨</button>
868
+ <button class="generate-btn" onclick="copyContent('notesContent')" style="background: linear-gradient(135deg, #118ab2, #06d6a0);">📋 Copy</button>
869
+ <button class="generate-btn" onclick="exportSectionPDF('notesContent')" style="background:linear-gradient(135deg, #06d6a0, #00c984);">📄 PDF</button>
870
+ </div>
871
+
872
+ <div class="panel-content" id="notesContent">
873
+ <div class="empty-state">
874
+ <div class="empty-icon">📌</div>
875
+ <div>Generate sticky notes with key points from the lecture.</div>
876
+ </div>
877
+ </div>
878
+ </div>
879
+
880
+ <!-- FLOWCHART PANEL -->
881
+ <div class="content-panel" id="panel-flowchart">
882
+ <div class="panel-header">
883
+ <div class="panel-title">🔄 Concept Flowchart</div>
884
+ <button class="generate-btn" id="flowchartBtn" onclick="generateFlowchart()">Generate ✨</button>
885
+ <button class="generate-btn" onclick="exportSectionPDF('flowchartContent')" style="background:linear-gradient(135deg, #06d6a0, #00c984);">📄 PDF</button>
886
+ </div>
887
+ <div class="panel-content" id="flowchartContent">
888
+ <div class="empty-state">
889
+ <div class="empty-icon">🔄</div>
890
+ <div>Generate a visual flowchart showing how concepts in the lecture connect.</div>
891
+ </div>
892
+ </div>
893
+ </div>
894
+ </div>
895
+ <!-- panel-transcript -->
896
+ <div class="content-panel" id="panel-transcript">
897
+ <div class="panel-header">
898
+ <div class="panel-title">📜 Full Transcript</div>
899
+ <button class="generate-btn" onclick="exportSectionPDF('transcriptContent')" style="background:linear-gradient(135deg, #06d6a0, #00c984);">📄 PDF</button>
900
+ </div>
901
+ <div class="panel-content" id="transcriptContent">
902
+ <div class="empty-state">
903
+ <div class="empty-icon">📜</div>
904
+ <div>Process a video to see transcript.</div>
905
+ </div>
906
+ </div>
907
+ </div>
908
+ <!-- quiz panel -->
909
+ <div class="content-panel" id="panel-quiz">
910
+ <div class="panel-header">
911
+ <div class="panel-title">🧠 Quiz Mode</div>
912
+ <button class="generate-btn" onclick="generateQuiz()">Generate ✨</button>
913
+ </div>
914
+ <div class="panel-content" id="quizContent">
915
+ <div class="empty-state">
916
+ <div class="empty-icon">🧠</div>
917
+ <div>Generate a quiz to test your knowledge!</div>
918
+ </div>
919
+ </div>
920
+ <div id="quizScore" style="display:none; padding:12px; text-align:center; font-family:'Syne',sans-serif; font-size:18px; color:var(--accent3);"></div>
921
+ </div>
922
+ <!-- CAMPARE VEDIO -->
923
+ <div class="content-panel" id="panel-compare">
924
+ <div class="panel-header">
925
+ <div class="panel-title">🔀 Compare Videos</div>
926
+ <button class="generate-btn" onclick="compareVideos()">Compare ✨</button>
927
+ </div>
928
+ <div style="padding:12px; display:flex; gap:8px;">
929
+ <input type="text" id="compareUrl" class="url-input" placeholder="Enter second YouTube URL...">
930
+ </div>
931
+ <div class="panel-content" id="compareContent">
932
+ <div class="empty-state">
933
+ <div class="empty-icon">🔀</div>
934
+ <div>Enter second video URL to compare!</div>
935
+ </div>
936
+ </div>
937
+ </div>
938
+ <!-- RIGHT: CHAT -->
939
+ <div class="chat-panel">
940
+ <div class="chat-header">
941
+ <div class="chat-dot"></div>
942
+ <div class="chat-title">Ask LectureLens AI</div>
943
+ </div>
944
+ <div class="chat-messages" id="chatMessages">
945
+ <div class="msg ai">
946
+ 👋 Hi! I'm LectureLens AI. Process a YouTube lecture and then ask me anything about it. I'll answer based strictly on the video content!
947
+ </div>
948
+ </div>
949
+ <div class="chat-input-row">
950
+ <textarea class="chat-input" id="chatInput" rows="2"
951
+ placeholder="Ask about the lecture..."
952
+ onkeydown="if(event.key==='Enter' && !event.shiftKey){ event.preventDefault(); sendMessage(); }"></textarea>
953
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">
954
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="white">
955
+ <path d="M2 21l21-9L2 3v7l15 2-15 2v7z"/>
956
+ </svg>
957
+ </button>
958
+ </div>
959
+ </div>
960
+ </div>
961
+ </div>
962
+
963
+ <script>
964
+ let currentSessionId = null;
965
+ let currentLanguage = 'english';
966
+
967
+ function setLang(lang, btn) {
968
+ currentLanguage = lang;
969
+ document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
970
+ btn.classList.add('active');
971
+ }
972
+
973
+ function showStatus(type, message) {
974
+ const bar = document.getElementById('statusBar');
975
+ const text = document.getElementById('statusText');
976
+ const spinner = document.getElementById('statusSpinner');
977
+ bar.className = 'status-bar ' + type;
978
+ text.textContent = message;
979
+ spinner.style.display = type === 'loading' ? 'block' : 'none';
980
+ if (type !== 'loading') {
981
+ setTimeout(() => { bar.className = 'status-bar'; }, 4000);
982
+ }
983
+ }
984
+
985
+ async function processVideo() {
986
+ const url = document.getElementById('urlInput').value.trim();
987
+ if (!url) { showStatus('error', 'Please enter a YouTube URL'); return; }
988
+
989
+ const btn = document.getElementById('processBtn');
990
+ btn.disabled = true;
991
+ btn.textContent = '⏳ Processing...';
992
+ showStatus('loading', 'Extracting transcript from YouTube...');
993
+
994
+ try {
995
+ const res = await fetch('/api/process', {
996
+ method: 'POST',
997
+ headers: { 'Content-Type': 'application/json' },
998
+ body: JSON.stringify({ url })
999
+ });
1000
+ const data = await res.json();
1001
+
1002
+ if (data.success) {
1003
+ currentSessionId = data.session_id;
1004
+ showStatus('success', `✅ Video processed! ${data.transcript_length} characters extracted.`);
1005
+
1006
+ document.getElementById('videoTitle').textContent = data.title;
1007
+ if (data.video_id) {
1008
+ const thumbImg = document.getElementById('videoThumbnail');
1009
+ thumbImg.style.display = 'block';
1010
+ thumbImg.src = `https://i.ytimg.com/vi/${data.video_id}/mqdefault.jpg`;
1011
+ thumbImg.onerror = () => { thumbImg.style.display = 'none'; };
1012
+ }
1013
+ document.getElementById('videoMeta').textContent =
1014
+ `${data.transcript_length.toLocaleString()} characters · Session ready`;
1015
+ document.getElementById('videoInfoBar').classList.add('visible');
1016
+ document.getElementById('mainGrid').classList.add('visible');
1017
+
1018
+ addMessage('ai', `🎓 I've analyzed the lecture "${data.title}". You can now ask me questions, generate summaries, flashcards, sticky notes, or a flowchart!`);
1019
+ if (data.transcript) {
1020
+ document.getElementById('transcriptContent').innerHTML =
1021
+ `<div style="line-height:1.8; font-size:13px; color:var(--text); white-space:pre-wrap">${data.transcript}</div>`;
1022
+ }
1023
+
1024
+ } else {
1025
+ showStatus('error', data.error || 'Failed to process video');
1026
+ }
1027
+ } catch (e) {
1028
+ showStatus('error', 'Network error. Please try again.');
1029
+ }
1030
+
1031
+ btn.disabled = false;
1032
+ btn.textContent = '🚀 Analyze Video';
1033
+ }
1034
+
1035
+ function showTab(tab, btn) {
1036
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
1037
+ document.querySelectorAll('.content-panel').forEach(p => p.classList.remove('active'));
1038
+ btn.classList.add('active');
1039
+ document.getElementById('panel-' + tab).classList.add('active');
1040
+ }
1041
+
1042
+ async function generateSummary() {
1043
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1044
+ const btn = document.getElementById('summaryBtn');
1045
+ const content = document.getElementById('summaryContent');
1046
+ btn.disabled = true;
1047
+ btn.textContent = '⏳ Generating...';
1048
+ content.innerHTML = '<div class="panel-loading visible"><div class="big-spinner"></div><div>Generating summary...</div></div>';
1049
+
1050
+ try {
1051
+ const res = await fetch('/api/summarize', {
1052
+ method: 'POST',
1053
+ headers: { 'Content-Type': 'application/json' },
1054
+ body: JSON.stringify({ session_id: currentSessionId, language: currentLanguage })
1055
+ });
1056
+ const data = await res.json();
1057
+ if (data.success) {
1058
+ const formatted = data.summary
1059
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
1060
+ .replace(/^## (.+)$/gm, '<h2>$2</h2>')
1061
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
1062
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
1063
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
1064
+ .replace(/^(\d+)\. (.+)$/gm, '<li>$2</li>')
1065
+ .replace(/\n\n/g, '</p><p>')
1066
+ .replace(/\n/g, '<br>');
1067
+ content.innerHTML = `<div class="summary-text"><p>${formatted}</p></div>`;
1068
+ } else {
1069
+ content.innerHTML = `<div class="empty-state"><div>❌ ${data.error}</div></div>`;
1070
+ }
1071
+ } catch (e) {
1072
+ content.innerHTML = '<div class="empty-state"><div>❌ Network error</div></div>';
1073
+ }
1074
+ btn.disabled = false;
1075
+ btn.textContent = 'Generate ✨';
1076
+ }
1077
+
1078
+ async function generateFlashcards() {
1079
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1080
+ const btn = document.getElementById('flashcardsBtn');
1081
+ const content = document.getElementById('flashcardsContent');
1082
+ btn.disabled = true;
1083
+ btn.textContent = '⏳ Generating...';
1084
+ content.innerHTML = '<div class="panel-loading visible"><div class="big-spinner"></div><div>Creating flashcards...</div></div>';
1085
+
1086
+ try {
1087
+ const res = await fetch('/api/flashcards', {
1088
+ method: 'POST',
1089
+ headers: { 'Content-Type': 'application/json' },
1090
+ body: JSON.stringify({ session_id: currentSessionId, language: currentLanguage })
1091
+ });
1092
+ const data = await res.json();
1093
+ if (data.success && data.flashcards.length > 0) {
1094
+ const html = data.flashcards.map((card, i) => `
1095
+ <div class="flashcard" onclick="this.classList.toggle('open')">
1096
+ <div class="card-question">
1097
+ <span class="card-q-badge">Q${i+1}</span>
1098
+ ${card.question}
1099
+ </div>
1100
+ <div class="card-answer">
1101
+ <span class="card-a-badge">ANSWER</span><br>
1102
+ ${card.answer}
1103
+ </div>
1104
+ </div>
1105
+ `).join('');
1106
+ content.innerHTML = `<div class="flashcards-grid">${html}</div>`;
1107
+ } else {
1108
+ content.innerHTML = `<div class="empty-state"><div>❌ ${data.error || 'Could not generate flashcards'}</div></div>`;
1109
+ }
1110
+ } catch (e) {
1111
+ content.innerHTML = '<div class="empty-state"><div>❌ Network error</div></div>';
1112
+ }
1113
+ btn.disabled = false;
1114
+ btn.textContent = 'Generate ✨';
1115
+ }
1116
+
1117
+ async function generateNotes() {
1118
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1119
+ const btn = document.getElementById('notesBtn');
1120
+ const content = document.getElementById('notesContent');
1121
+ btn.disabled = true;
1122
+ btn.textContent = '⏳ Generating...';
1123
+ content.innerHTML = '<div class="panel-loading visible"><div class="big-spinner"></div><div>Creating sticky notes...</div></div>';
1124
+
1125
+ try {
1126
+ const res = await fetch('/api/notes', {
1127
+ method: 'POST',
1128
+ headers: { 'Content-Type': 'application/json' },
1129
+ body: JSON.stringify({ session_id: currentSessionId, language: currentLanguage })
1130
+ });
1131
+ const data = await res.json();
1132
+ if (data.success && data.notes.length > 0) {
1133
+ const html = data.notes.map(note => `
1134
+ <div class="sticky-note ${note.color || 'yellow'}">
1135
+ <div class="note-title">${note.title}</div>
1136
+ <div class="note-content">${note.content}</div>
1137
+ </div>
1138
+ `).join('');
1139
+ content.innerHTML = `<div class="notes-grid">${html}</div>`;
1140
+ } else {
1141
+ content.innerHTML = `<div class="empty-state"><div>❌ ${data.error || 'Could not generate notes'}</div></div>`;
1142
+ }
1143
+ } catch (e) {
1144
+ content.innerHTML = '<div class="empty-state"><div>❌ Network error</div></div>';
1145
+ }
1146
+ btn.disabled = false;
1147
+ btn.textContent = 'Generate ✨';
1148
+ }
1149
+
1150
+ async function generateFlowchart() {
1151
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1152
+ const btn = document.getElementById('flowchartBtn');
1153
+ const content = document.getElementById('flowchartContent');
1154
+ btn.disabled = true;
1155
+ btn.textContent = '⏳ Generating...';
1156
+ content.innerHTML = '<div class="panel-loading visible"><div class="big-spinner"></div><div>Building flowchart...</div></div>';
1157
+
1158
+ try {
1159
+ const res = await fetch('/api/flowchart', {
1160
+ method: 'POST',
1161
+ headers: { 'Content-Type': 'application/json' },
1162
+ body: JSON.stringify({ session_id: currentSessionId, language: currentLanguage })
1163
+ });
1164
+ const data = await res.json();
1165
+ if (data.success && data.flowchart.nodes) {
1166
+ const fc = data.flowchart;
1167
+ // Build adjacency for ordering
1168
+ const nodeMap = {};
1169
+ fc.nodes.forEach(n => nodeMap[n.id] = n);
1170
+
1171
+ // Simple linear render (ordered by edges)
1172
+ const visited = new Set();
1173
+ const ordered = [];
1174
+ const edgeMap = {};
1175
+ fc.edges.forEach(e => { edgeMap[e.from] = e.to; });
1176
+
1177
+ // Find start node
1178
+ const targets = new Set(fc.edges.map(e => e.to));
1179
+ let start = fc.nodes.find(n => !targets.has(n.id) || n.type === 'start');
1180
+ if (!start) start = fc.nodes[0];
1181
+
1182
+ let cur = start;
1183
+ while (cur && !visited.has(cur.id)) {
1184
+ ordered.push(cur);
1185
+ visited.add(cur.id);
1186
+ const nextId = edgeMap[cur.id];
1187
+ cur = nextId ? nodeMap[nextId] : null;
1188
+ }
1189
+ // Add any remaining nodes
1190
+ fc.nodes.forEach(n => { if (!visited.has(n.id)) ordered.push(n); });
1191
+
1192
+ const html = ordered.map((node, i) => `
1193
+ <div class="flow-node ${node.type || 'process'}">${node.label}</div>
1194
+ ${i < ordered.length - 1 ? '<div class="flow-arrow"></div>' : ''}
1195
+ `).join('');
1196
+ content.innerHTML = `<div class="flowchart-container">${html}</div>`;
1197
+ } else {
1198
+ content.innerHTML = `<div class="empty-state"><div>❌ ${data.error || 'Could not generate flowchart'}</div></div>`;
1199
+ }
1200
+ } catch (e) {
1201
+ content.innerHTML = '<div class="empty-state"><div>❌ Network error</div></div>';
1202
+ }
1203
+ btn.disabled = false;
1204
+ btn.textContent = 'Generate ✨';
1205
+ }
1206
+ async function exportPDF() {
1207
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1208
+
1209
+ const summaryContent = document.getElementById('summaryContent').innerText;
1210
+ if (!summaryContent || summaryContent.includes('Click "Generate"')) {
1211
+ alert('Please generate a summary first!');
1212
+ return;
1213
+ }
1214
+
1215
+ try {
1216
+ const res = await fetch('/api/export', {
1217
+ method: 'POST',
1218
+ headers: { 'Content-Type': 'application/json' },
1219
+ body: JSON.stringify({
1220
+ session_id: currentSessionId,
1221
+ content: summaryContent,
1222
+ title: document.getElementById('videoTitle').textContent
1223
+ })
1224
+ });
1225
+
1226
+ if (res.ok) {
1227
+ const blob = await res.blob();
1228
+ const url = window.URL.createObjectURL(blob);
1229
+ const a = document.createElement('a');
1230
+ a.href = url;
1231
+ a.download = 'lecturelens_export.pdf';
1232
+ a.click();
1233
+ }
1234
+ } catch (e) {
1235
+ alert('Export failed. Please try again.');
1236
+ }
1237
+ }
1238
+ function addMessage(role, text) {
1239
+ const container = document.getElementById('chatMessages');
1240
+ const div = document.createElement('div');
1241
+ div.className = 'msg ' + role;
1242
+ div.textContent = text;
1243
+ container.appendChild(div);
1244
+ container.scrollTop = container.scrollHeight;
1245
+ return div;
1246
+ }
1247
+
1248
+ function addTypingIndicator() {
1249
+ const container = document.getElementById('chatMessages');
1250
+ const div = document.createElement('div');
1251
+ div.className = 'msg ai typing';
1252
+ div.id = 'typingIndicator';
1253
+ div.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>';
1254
+ container.appendChild(div);
1255
+ container.scrollTop = container.scrollHeight;
1256
+ }
1257
+
1258
+ function removeTypingIndicator() {
1259
+ const el = document.getElementById('typingIndicator');
1260
+ if (el) el.remove();
1261
+ }
1262
+ function copyContent(elementId) {
1263
+ const text = document.getElementById(elementId).innerText;
1264
+ navigator.clipboard.writeText(text).then(() => {
1265
+ alert('✅ Copied to clipboard!');
1266
+ }).catch(() => {
1267
+ alert('��� Copy failed. Please try manually.');
1268
+ });
1269
+ }
1270
+
1271
+
1272
+ async function exportSectionPDF(elementId) {
1273
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1274
+ const content = document.getElementById(elementId).innerText;
1275
+ if (!content || content.includes('Generate')) {
1276
+ alert('Please generate content first!');
1277
+ return;
1278
+ }
1279
+ try {
1280
+ const res = await fetch('/api/export', {
1281
+ method: 'POST',
1282
+ headers: { 'Content-Type': 'application/json' },
1283
+ body: JSON.stringify({
1284
+ session_id: currentSessionId,
1285
+ content: content,
1286
+ title: document.getElementById('videoTitle').textContent
1287
+ })
1288
+ });
1289
+ if (res.ok) {
1290
+ const blob = await res.blob();
1291
+ const url = window.URL.createObjectURL(blob);
1292
+ const a = document.createElement('a');
1293
+ a.href = url;
1294
+ a.download = `${elementId}_export.pdf`;
1295
+ a.click();
1296
+ }
1297
+ } catch(e) {
1298
+ alert('Export failed!');
1299
+ }
1300
+ }
1301
+
1302
+ let quizData = [];
1303
+ let currentQ = 0;
1304
+ let score = 0;
1305
+
1306
+ async function generateQuiz() {
1307
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1308
+ const content = document.getElementById('quizContent');
1309
+ content.innerHTML = '<div class="panel-loading visible"><div class="big-spinner"></div><div>Generating quiz...</div></div>';
1310
+
1311
+ const res = await fetch('/api/quiz', {
1312
+ method: 'POST',
1313
+ headers: { 'Content-Type': 'application/json' },
1314
+ body: JSON.stringify({ session_id: currentSessionId, language: currentLanguage })
1315
+ });
1316
+ const data = await res.json();
1317
+
1318
+ if (data.success && data.questions.length > 0) {
1319
+ quizData = data.questions;
1320
+ currentQ = 0;
1321
+ score = 0;
1322
+ document.getElementById('quizScore').style.display = 'none';
1323
+ showQuestion();
1324
+ }
1325
+ }
1326
+
1327
+ function showQuestion() {
1328
+ if (currentQ >= quizData.length) {
1329
+ document.getElementById('quizContent').innerHTML = '';
1330
+ document.getElementById('quizScore').style.display = 'block';
1331
+ document.getElementById('quizScore').innerHTML = `🎉 Score: ${score}/${quizData.length}`;
1332
+ return;
1333
+ }
1334
+ const q = quizData[currentQ];
1335
+ const html = `
1336
+ <div style="padding:16px;">
1337
+ <div style="font-weight:600; margin-bottom:16px; color:var(--text)">Q${currentQ+1}: ${q.question}</div>
1338
+ ${q.options.map(opt => `
1339
+ <button onclick="checkAnswer('${opt[0]}', '${q.correct}')"
1340
+ style="display:block; width:100%; text-align:left; padding:10px 16px; margin-bottom:8px;
1341
+ background:var(--surface2); border:1px solid var(--border); border-radius:8px;
1342
+ color:var(--text); cursor:pointer; font-size:13px;">
1343
+ ${opt}
1344
+ </button>`).join('')}
1345
+ </div>`;
1346
+ document.getElementById('quizContent').innerHTML = html;
1347
+ }
1348
+
1349
+ function checkAnswer(selected, correct) {
1350
+ if (selected === correct) score++;
1351
+ currentQ++;
1352
+ showQuestion();
1353
+ }
1354
+ async function sendMessage() {
1355
+ if (!currentSessionId) {
1356
+ addMessage('ai', '⚠️ Please process a YouTube video first before asking questions.');
1357
+ return;
1358
+ }
1359
+ const input = document.getElementById('chatInput');
1360
+ const question = input.value.trim();
1361
+ if (!question) return;
1362
+
1363
+ addMessage('user', question);
1364
+ input.value = '';
1365
+ addTypingIndicator();
1366
+
1367
+ const sendBtn = document.getElementById('sendBtn');
1368
+ sendBtn.disabled = true;
1369
+
1370
+ try {
1371
+ const res = await fetch('/api/chat', {
1372
+ method: 'POST',
1373
+ headers: { 'Content-Type': 'application/json' },
1374
+ body: JSON.stringify({
1375
+ session_id: currentSessionId,
1376
+ question,
1377
+ language: currentLanguage
1378
+ })
1379
+ });
1380
+ const data = await res.json();
1381
+ removeTypingIndicator();
1382
+ addMessage('ai', data.answer || data.error || 'Sorry, I could not answer that.');
1383
+ } catch (e) {
1384
+ removeTypingIndicator();
1385
+ addMessage('ai', '❌ Network error. Please try again.');
1386
+ }
1387
+ sendBtn.disabled = false;
1388
+ }
1389
+
1390
+
1391
+ async function compareVideos() {
1392
+ if (!currentSessionId) { alert('Please process a video first.'); return; }
1393
+ const url2 = document.getElementById('compareUrl').value.trim();
1394
+ if (!url2) { alert('Please enter second video URL!'); return; }
1395
+
1396
+ const content = document.getElementById('compareContent');
1397
+ content.innerHTML = '<div class="panel-loading visible"><div class="big-spinner"></div><div>Comparing videos...</div></div>';
1398
+
1399
+ try {
1400
+ const res = await fetch('/api/compare', {
1401
+ method: 'POST',
1402
+ headers: { 'Content-Type': 'application/json' },
1403
+ body: JSON.stringify({
1404
+ session_id: currentSessionId,
1405
+ url2: url2,
1406
+ language: currentLanguage
1407
+ })
1408
+ });
1409
+ const data = await res.json();
1410
+ if (data.success) {
1411
+ content.innerHTML = `<div class="summary-text" style="padding:8px"><p>${data.comparison.replace(/\n/g, '<br>')}</p></div>`;
1412
+ } else {
1413
+ content.innerHTML = `<div class="empty-state"><div>❌ ${data.error}</div></div>`;
1414
+ }
1415
+ } catch(e) {
1416
+ content.innerHTML = '<div class="empty-state"><div>❌ Network error</div></div>';
1417
+ }
1418
+ }
1419
+ </script>
1420
+ </body>
1421
+ </html>
1422
+
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # LectureLens AI - Utilities Package
utils/embedder.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from sklearn.feature_extraction.text import TfidfVectorizer
3
+ from sklearn.metrics.pairwise import cosine_similarity
4
+
5
+ class EmbeddingHandler:
6
+ def __init__(self):
7
+ print("TF-IDF Search ready — free, no download!")
8
+ self.chunks_store = {}
9
+ self.vectorizers = {}
10
+ self.matrices = {}
11
+
12
+ def process_and_store(self, transcript: str, session_id: str):
13
+ chunks = self._chunk_transcript(transcript)
14
+ vectorizer = TfidfVectorizer(stop_words='english')
15
+ matrix = vectorizer.fit_transform(chunks)
16
+ self.chunks_store[session_id] = chunks
17
+ self.vectorizers[session_id] = vectorizer
18
+ self.matrices[session_id] = matrix
19
+ print(f"Stored {len(chunks)} chunks for session {session_id}")
20
+
21
+ def retrieve(self, query: str, session_id: str, top_k: int = 5) -> str:
22
+ if session_id not in self.chunks_store:
23
+ raise ValueError("Session not found.")
24
+ vectorizer = self.vectorizers[session_id]
25
+ matrix = self.matrices[session_id]
26
+ chunks = self.chunks_store[session_id]
27
+ query_vec = vectorizer.transform([query])
28
+ scores = cosine_similarity(query_vec, matrix).flatten()
29
+ top_indices = scores.argsort()[-top_k:][::-1]
30
+ top_chunks = [chunks[i] for i in top_indices]
31
+ return "\n\n---\n\n".join(top_chunks)
32
+
33
+ def _chunk_transcript(self, transcript: str, chunk_size: int = 400, overlap: int = 50) -> list:
34
+ words = transcript.split()
35
+ chunks = []
36
+ for i in range(0, len(words), chunk_size - overlap):
37
+ chunk = " ".join(words[i:i + chunk_size])
38
+ if chunk:
39
+ chunks.append(chunk)
40
+ return chunks
41
+
42
+ def cleanup_session(self, session_id: str):
43
+ for store in [self.chunks_store, self.vectorizers, self.matrices]:
44
+ if session_id in store:
45
+ del store[session_id]
utils/llm_handler.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ from openai import OpenAI
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ class LLMHandler:
10
+ def __init__(self):
11
+ api_key = os.environ.get("OPENAI_API_KEY")
12
+ if not api_key:
13
+ raise ValueError("OPENAI_API_KEY environment variable not set!")
14
+ self.client = OpenAI(api_key=api_key)
15
+ self.model = "gpt-4o-mini"
16
+
17
+ def _call_llm(self, system_prompt: str, user_prompt: str, max_tokens: int = 2000) -> str:
18
+ response = self.client.chat.completions.create(
19
+ model=self.model,
20
+ messages=[
21
+ {"role": "system", "content": system_prompt},
22
+ {"role": "user", "content": user_prompt}
23
+ ],
24
+ max_tokens=max_tokens,
25
+ temperature=0.3
26
+ )
27
+ return response.choices[0].message.content
28
+
29
+ def _get_language_instruction(self, language: str) -> str:
30
+ instructions = {
31
+ "english": "Respond in clear, simple English.",
32
+ "urdu": "صرف اردو میں جواب دیں۔ آسان اردو استعمال کریں۔",
33
+ "roman_urdu": "Sirf Roman Urdu mein jawab do. English bilkul mat use karo. Jaise: 'Yeh topic bohot important hai kyunki...'"
34
+ }
35
+ return instructions.get(language, instructions["english"])
36
+
37
+ def answer_question(self, question: str, context: str, language: str, video_title: str, history: list = []) -> str:
38
+ lang_instruction = self._get_language_instruction(language)
39
+
40
+ system_prompt = f"""You are LectureLens AI, an intelligent study assistant.
41
+ You ONLY answer based on the video transcript provided.
42
+ If the answer is not in the transcript, say "This information is not covered in the video."
43
+ {lang_instruction}"""
44
+
45
+ messages = [{"role": "system", "content": system_prompt}]
46
+
47
+ for msg in history[-6:]:
48
+ messages.append({"role": msg["role"], "content": msg["content"]})
49
+
50
+ messages.append({
51
+ "role": "user",
52
+ "content": f"""Video: "{video_title}"
53
+ Relevant transcript:
54
+ {context}
55
+ Question: {question}"""
56
+ })
57
+
58
+ response = self.client.chat.completions.create(
59
+ model=self.model,
60
+ messages=messages,
61
+ max_tokens=2000,
62
+ temperature=0.3
63
+ )
64
+ return response.choices[0].message.content
65
+
66
+ def summarize(self, transcript: str, language: str, video_title: str) -> str:
67
+ lang_instruction = self._get_language_instruction(language)
68
+ transcript_excerpt = transcript[:6000]
69
+
70
+ system_prompt = f"""You are LectureLens AI, an expert at creating educational summaries.
71
+ {lang_instruction}
72
+ Create well-structured, comprehensive summaries that help students understand key concepts."""
73
+
74
+ user_prompt = f"""Create a detailed summary of this lecture: "{video_title}"
75
+
76
+ Transcript:
77
+ {transcript_excerpt}
78
+
79
+ Please include:
80
+ 1. Main Topic Overview
81
+ 2. Key Concepts Covered (with brief explanations)
82
+ 3. Important Points to Remember
83
+ 4. Conclusion
84
+
85
+ Format it clearly with headings."""
86
+
87
+ return self._call_llm(system_prompt, user_prompt, max_tokens=2500)
88
+
89
+ def generate_flashcards(self, transcript: str, language: str, video_title: str) -> list:
90
+ lang_instruction = self._get_language_instruction(language)
91
+ transcript_excerpt = transcript[:5000]
92
+
93
+ system_prompt = f"""You are LectureLens AI. Generate educational flashcards.
94
+ {lang_instruction}
95
+ IMPORTANT: Return ONLY valid JSON, no other text.
96
+ Format: [{{"question": "...", "answer": "..."}}]"""
97
+
98
+ user_prompt = f"""Create 10 flashcards from this lecture: "{video_title}"
99
+
100
+ Transcript:
101
+ {transcript_excerpt}
102
+
103
+ Return ONLY a JSON array of flashcards with "question" and "answer" fields.
104
+ Make questions test understanding, not just memory."""
105
+
106
+ response = self._call_llm(system_prompt, user_prompt, max_tokens=2000)
107
+
108
+ try:
109
+ json_match = re.search(r'\[.*\]', response, re.DOTALL)
110
+ if json_match:
111
+ cards = json.loads(json_match.group())
112
+ return cards
113
+ except:
114
+ pass
115
+
116
+ return [{"question": "What is the main topic of this lecture?",
117
+ "answer": "Please refer to the summary for the main topics covered."}]
118
+
119
+ def generate_notes(self, transcript: str, language: str, video_title: str) -> list:
120
+ lang_instruction = self._get_language_instruction(language)
121
+ transcript_excerpt = transcript[:5000]
122
+
123
+ system_prompt = f"""You are LectureLens AI. Generate concise sticky notes for studying.
124
+ {lang_instruction}
125
+ IMPORTANT: Return ONLY valid JSON, no other text.
126
+ Format: [{{"title": "...", "content": "...", "color": "yellow|blue|green|pink|purple"}}]"""
127
+
128
+ user_prompt = f"""Create 8 sticky notes from this lecture: "{video_title}"
129
+
130
+ Transcript:
131
+ {transcript_excerpt}
132
+
133
+ Return ONLY a JSON array. Each note should have:
134
+ - title: short topic name (3-5 words)
135
+ - content: key information (2-3 sentences)
136
+ - color: one of yellow, blue, green, pink, purple"""
137
+
138
+ response = self._call_llm(system_prompt, user_prompt, max_tokens=2000)
139
+
140
+ try:
141
+ json_match = re.search(r'\[.*\]', response, re.DOTALL)
142
+ if json_match:
143
+ notes = json.loads(json_match.group())
144
+ return notes
145
+ except:
146
+ pass
147
+
148
+ return [{"title": "Key Point", "content": "Process the video to see notes.", "color": "yellow"}]
149
+
150
+ def generate_flowchart(self, transcript: str, language: str, video_title: str) -> dict:
151
+ lang_instruction = self._get_language_instruction(language)
152
+ transcript_excerpt = transcript[:5000]
153
+
154
+ system_prompt = f"""You are LectureLens AI. Generate flowchart data for lecture content.
155
+ {lang_instruction}
156
+ IMPORTANT: Return ONLY valid JSON, no other text.
157
+ Format: {{"title": "...", "nodes": [{{"id": "1", "label": "...", "type": "start|process|decision|end"}}], "edges": [{{"from": "1", "to": "2", "label": ""}}]}}"""
158
+
159
+ user_prompt = f"""Create a flowchart showing the main concepts and flow of: "{video_title}"
160
+
161
+ Transcript:
162
+ {transcript_excerpt}
163
+
164
+ Return ONLY a JSON object with nodes and edges showing how concepts connect.
165
+ Use 8-12 nodes maximum."""
166
+
167
+ response = self._call_llm(system_prompt, user_prompt, max_tokens=2000)
168
+
169
+ try:
170
+ json_match = re.search(r'\{.*\}', response, re.DOTALL)
171
+ if json_match:
172
+ flowchart = json.loads(json_match.group())
173
+ return flowchart
174
+ except:
175
+ pass
176
+
177
+ return {
178
+ "title": video_title,
179
+ "nodes": [
180
+ {"id": "1", "label": "Video Start", "type": "start"},
181
+ {"id": "2", "label": "Main Content", "type": "process"},
182
+ {"id": "3", "label": "Key Concepts", "type": "process"},
183
+ {"id": "4", "label": "Conclusion", "type": "end"}
184
+ ],
185
+ "edges": [
186
+ {"from": "1", "to": "2", "label": ""},
187
+ {"from": "2", "to": "3", "label": ""},
188
+ {"from": "3", "to": "4", "label": ""}
189
+ ]
190
+ }
191
+ def generate_quiz(self, transcript: str, language: str, video_title: str) -> list:
192
+ lang_instruction = self._get_language_instruction(language)
193
+ transcript_excerpt = transcript[:5000]
194
+
195
+ system_prompt = f"""You are LectureLens AI. Generate MCQ quiz questions.
196
+ {lang_instruction}
197
+ IMPORTANT: Return ONLY a valid JSON array. No markdown, no backticks, no explanation.
198
+ Exact format: [{{"question":"...","options":["A) ...","B) ...","C) ...","D) ..."],"correct":"A"}}]"""
199
+
200
+ user_prompt = f"""Create 5 MCQ questions from: "{video_title}"
201
+ Transcript: {transcript_excerpt}
202
+ Return ONLY JSON array, nothing else."""
203
+
204
+ try:
205
+ response = self._call_llm(system_prompt, user_prompt, max_tokens=2000)
206
+ clean = re.sub(r'```json|```', '', response).strip()
207
+ try:
208
+ return json.loads(clean)
209
+ except:
210
+ json_match = re.search(r'\[.*\]', clean, re.DOTALL)
211
+ if json_match:
212
+ return json.loads(json_match.group())
213
+ except Exception as e:
214
+ print(f"Quiz error: {str(e)}")
215
+ return []
216
+
217
+ def compare_videos(self, transcript1: str, title1: str, transcript2: str, title2: str, language: str) -> str:
218
+ lang_instruction = self._get_language_instruction(language)
219
+ system_prompt = f"""You are LectureLens AI. Compare two lecture videos.
220
+ {lang_instruction}"""
221
+ user_prompt = f"""Compare these two lectures:
222
+
223
+ Video 1: "{title1}"
224
+ {transcript1[:3000]}
225
+
226
+ Video 2: "{title2}"
227
+ {transcript2[:3000]}
228
+
229
+ Include:
230
+ 1. Common Topics
231
+ 2. Unique Points in Video 1
232
+ 3. Unique Points in Video 2
233
+ 4. Which is better for beginners?"""
234
+ return self._call_llm(system_prompt, user_prompt, max_tokens=2000)
235
+
236
+
237
+
238
+
239
+ def check_educational(self, transcript: str, title: str = "") -> bool:
240
+
241
+ reject_keywords = [
242
+ 'recipe', 'cooking', 'cook', 'ingredient', 'tablespoon', 'teaspoon',
243
+ 'karahi', 'biryani', 'curry', 'masala', 'chawal', 'gosht', 'daal',
244
+ 'pakana', 'khana', 'paka', 'tail', 'namak', 'mirch', 'aata', 'maida',
245
+ 'drama', 'episode', 'serial', 'actor', 'actress', 'scene',
246
+ 'song', 'music', 'singer', 'lyrics', 'concert', 'album',
247
+ 'cartoon', 'animation', 'anime', 'character',
248
+ 'vlog', 'makeup', 'beauty', 'skincare', 'fashion', 'outfit',
249
+ 'subscribe', 'like karo', 'follow karo', 'instagram',
250
+ 'funny', 'comedy', 'prank', 'challenge',
251
+ 'news', 'breaking news', 'reporter', 'anchor',
252
+ 'game', 'gaming', 'gameplay', 'streamer',
253
+ 'travel', 'trip', 'tour', 'vlog',
254
+ 'reaction', 'review karte hain',
255
+ ]
256
+
257
+ accept_keywords = [
258
+ 'lecture', 'lesson', 'chapter', 'topic', 'concept', 'definition',
259
+ 'theory', 'algorithm', 'programming', 'code', 'function',
260
+ 'mathematics', 'math', 'physics', 'chemistry', 'biology',
261
+ 'history', 'geography', 'economics', 'psychology',
262
+ 'tutorial', 'course', 'university', 'college', 'school',
263
+ 'exam', 'assignment', 'hypothesis', 'equation', 'formula',
264
+ 'theorem', 'proof', 'data', 'analysis', 'research',
265
+ 'python', 'javascript', 'machine learning', 'artificial intelligence',
266
+ 'database', 'network', 'compiler', 'accounting', 'finance',
267
+ 'explain', 'understand', 'learn', 'study', 'education',
268
+ 'parh', 'seekhna', 'samajhna', 'taleem', 'ilm',
269
+ 'class', 'teacher', 'student', 'syllabus', 'notes',
270
+ ]
271
+
272
+ text = (transcript[:3000] + " " + title).lower()
273
+
274
+ reject_count = sum(1 for kw in reject_keywords if kw in text)
275
+ accept_count = sum(1 for kw in accept_keywords if kw in text)
276
+
277
+ # 2 ya zyada reject keywords — reject!
278
+ if reject_count >= 2:
279
+ return False
280
+
281
+ # 2 ya zyada accept keywords — allow!
282
+ if accept_count >= 2:
283
+ return True
284
+
285
+ # Dono mein kuch nahi — AI se check
286
+ try:
287
+ system_prompt = """Strict classifier. Return ONLY 'yes' or 'no'.
288
+ 'yes' ONLY for: university lecture, school lesson, coding tutorial, academic subject, professional skill training.
289
+ 'no' for: cooking, drama, song, vlog, news, cartoon, gaming, fashion, travel, comedy, recipe."""
290
+ user_prompt = f"Title: {title}\nTranscript: {transcript[:800]}\nIs this educational? yes or no only."
291
+ response = self._call_llm(system_prompt, user_prompt, max_tokens=5)
292
+ return 'yes' in response.lower().strip()
293
+ except:
294
+ return False
utils/transcript_handler.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from youtube_transcript_api import YouTubeTranscriptApi
2
+ from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
3
+ import re
4
+ import yt_dlp
5
+
6
+ def extract_video_id(url: str) -> str:
7
+ """Extract YouTube video ID from URL."""
8
+ patterns = [
9
+ r"(?:v=|\/)([0-9A-Za-z_-]{11}).*",
10
+ r"(?:youtu\.be\/)([0-9A-Za-z_-]{11})",
11
+ r"(?:embed\/)([0-9A-Za-z_-]{11})",
12
+ ]
13
+ for pattern in patterns:
14
+ match = re.search(pattern, url)
15
+ if match:
16
+ return match.group(1)
17
+ return None
18
+
19
+
20
+ def get_transcript(url: str) -> dict:
21
+ video_id = extract_video_id(url)
22
+
23
+ if not video_id:
24
+ return {"success": False, "error": "Invalid YouTube URL."}
25
+
26
+ try:
27
+ ytt_api = YouTubeTranscriptApi()
28
+ try:
29
+ transcript_data = ytt_api.fetch(video_id, languages=['en', 'hi', 'ur', 'en-US', 'en-GB'])
30
+ except:
31
+ transcript_list = ytt_api.list(video_id)
32
+ transcript_data = transcript_list.find_transcript(
33
+ [t.language_code for t in transcript_list]
34
+ ).fetch()
35
+
36
+ full_transcript = " ".join([entry.text for entry in transcript_data.snippets])
37
+ full_transcript = clean_transcript(full_transcript)
38
+
39
+ # ✅ Title fetch karo
40
+ try:
41
+ ydl_opts = {'quiet': True, 'skip_download': True}
42
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
43
+ info = ydl.extract_info(url, download=False)
44
+ video_title = info.get('title', f'Video {video_id}')
45
+ except:
46
+ video_title = f'Video {video_id}'
47
+
48
+ return {
49
+ "success": True,
50
+ "transcript": full_transcript,
51
+ "title": video_title, # ✅ Real title
52
+ "video_id": video_id,
53
+ }
54
+
55
+ except Exception as e:
56
+ return {"success": False, "error": f"Error extracting transcript: {str(e)}"}
57
+
58
+
59
+
60
+ def clean_transcript(text: str) -> str:
61
+ """Clean and normalize transcript text."""
62
+ # Remove music notations
63
+ text = re.sub(r'\[.*?\]', '', text)
64
+ text = re.sub(r'\(.*?\)', '', text)
65
+ # Remove extra whitespace
66
+ text = re.sub(r'\s+', ' ', text).strip()
67
+ # Remove common filler markers
68
+ text = text.replace('♪', '').replace('♫', '')
69
+ return text
70
+
71
+
72
+ def chunk_transcript(transcript: str, chunk_size: int = 500, overlap: int = 50) -> list:
73
+ """Split transcript into overlapping chunks for better retrieval."""
74
+ words = transcript.split()
75
+ chunks = []
76
+
77
+ for i in range(0, len(words), chunk_size - overlap):
78
+ chunk = " ".join(words[i:i + chunk_size])
79
+ if chunk:
80
+ chunks.append(chunk)
81
+
82
+ return chunks