santu578 commited on
Commit
0bb81de
·
1 Parent(s): a87727b

Add YouTube Comment Analyzer with FastAPI

Browse files
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies for PyTorch and transformers
6
+ RUN apt-get update && apt-get install -y \
7
+ gcc \
8
+ g++ \
9
+ git \
10
+ curl \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements first for better caching
14
+ COPY requirements.txt .
15
+ RUN pip install --no-cache-dir --upgrade pip
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy application code
19
+ COPY . .
20
+
21
+ # Create cache directory for models
22
+ RUN mkdir -p /app/cache /app/data/cache
23
+
24
+ # Set environment variables
25
+ ENV PYTHONUNBUFFERED=1
26
+ ENV HF_HOME=/app/cache
27
+ ENV TRANSFORMERS_CACHE=/app/cache
28
+ ENV TORCH_HOME=/app/cache
29
+ ENV PYTHONPATH=/app
30
+
31
+ # Expose Hugging Face Spaces default port
32
+ EXPOSE 7860
33
+
34
+ # Health check
35
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
36
+ CMD curl -f http://localhost:7860/health || exit 1
37
+
38
+ # Start the application
39
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
data/__pycache__/fetch_comments.cpython-311.pyc ADDED
Binary file (11.7 kB). View file
 
data/fetch_comments.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import re
3
+ import time
4
+ import sqlite3
5
+ import os
6
+ from datetime import datetime
7
+
8
+ # Your YouTube API key
9
+ API_KEY = "AIzaSyBwwLnHXKmL5VQhkKzumCS5r1Cbi3HdUro"
10
+
11
+ def extract_video_id(url):
12
+ """Extract video ID from YouTube URL"""
13
+ patterns = [
14
+ r'(?:youtube\.com\/watch\?v=)([\w-]+)',
15
+ r'(?:youtu\.be\/)([\w-]+)',
16
+ r'(?:youtube\.com\/embed\/)([\w-]+)',
17
+ r'(?:youtube\.com\/v\/)([\w-]+)',
18
+ r'(?:youtube\.com\/watch\?.*v=)([\w-]+)'
19
+ ]
20
+
21
+ for pattern in patterns:
22
+ match = re.search(pattern, url)
23
+ if match:
24
+ return match.group(1)
25
+ return None
26
+
27
+ # Database setup for caching large comment batches
28
+ DB_PATH = "youtube_cache.db"
29
+
30
+ def init_cache_db():
31
+ """Initialize SQLite cache database for storing fetched comments"""
32
+ conn = sqlite3.connect(DB_PATH)
33
+ cursor = conn.cursor()
34
+ cursor.execute('''
35
+ CREATE TABLE IF NOT EXISTS cached_comments (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ video_id TEXT,
38
+ comment_text TEXT,
39
+ fetched_at TIMESTAMP,
40
+ processed INTEGER DEFAULT 0,
41
+ sentiment TEXT,
42
+ sarcasm TEXT,
43
+ emotion TEXT
44
+ )
45
+ ''')
46
+ cursor.execute('''
47
+ CREATE INDEX IF NOT EXISTS idx_video_id ON cached_comments(video_id)
48
+ ''')
49
+ cursor.execute('''
50
+ CREATE INDEX IF NOT EXISTS idx_processed ON cached_comments(processed)
51
+ ''')
52
+ conn.commit()
53
+ conn.close()
54
+
55
+ def save_comments_to_cache(video_id, comments):
56
+ """Save fetched comments to local cache"""
57
+ conn = sqlite3.connect(DB_PATH)
58
+ cursor = conn.cursor()
59
+
60
+ # Clear old cache for this video first (optional - for fresh analysis)
61
+ # cursor.execute("DELETE FROM cached_comments WHERE video_id = ?", (video_id,))
62
+
63
+ for comment in comments:
64
+ cursor.execute('''
65
+ INSERT INTO cached_comments (video_id, comment_text, fetched_at, processed)
66
+ VALUES (?, ?, ?, 0)
67
+ ''', (video_id, comment, datetime.now()))
68
+
69
+ conn.commit()
70
+ conn.close()
71
+ print(f"💾 Saved {len(comments)} comments to cache")
72
+
73
+ def load_comments_from_cache(video_id, limit=None):
74
+ """Load comments from local cache"""
75
+ conn = sqlite3.connect(DB_PATH)
76
+ cursor = conn.cursor()
77
+
78
+ if limit:
79
+ cursor.execute('''
80
+ SELECT comment_text FROM cached_comments
81
+ WHERE video_id = ? AND processed = 0
82
+ LIMIT ?
83
+ ''', (video_id, limit))
84
+ else:
85
+ cursor.execute('''
86
+ SELECT comment_text FROM cached_comments
87
+ WHERE video_id = ? AND processed = 0
88
+ ''', (video_id,))
89
+
90
+ comments = [row[0] for row in cursor.fetchall()]
91
+ conn.close()
92
+ return comments
93
+
94
+ def get_cached_comment_count(video_id):
95
+ """Get count of cached comments for a video"""
96
+ conn = sqlite3.connect(DB_PATH)
97
+ cursor = conn.cursor()
98
+ cursor.execute('''
99
+ SELECT COUNT(*) FROM cached_comments
100
+ WHERE video_id = ? AND processed = 0
101
+ ''', (video_id,))
102
+ count = cursor.fetchone()[0]
103
+ conn.close()
104
+ return count
105
+
106
+ def get_comments_from_url(video_url, max_results=500):
107
+ """
108
+ Fetch YouTube comments with pagination support
109
+ Can fetch up to 10000+ comments with caching
110
+ """
111
+ try:
112
+ video_id = extract_video_id(video_url)
113
+
114
+ if not video_id:
115
+ print(f"Could not extract video ID from URL: {video_url}")
116
+ return []
117
+
118
+ print(f"Video ID: {video_id}")
119
+ print(f"Requested comments: {max_results}")
120
+
121
+ # Check cache first
122
+ init_cache_db()
123
+ cached_count = get_cached_comment_count(video_id)
124
+
125
+ if cached_count >= max_results:
126
+ print(f"✅ Using {cached_count} cached comments (no API call needed)")
127
+ return load_comments_from_cache(video_id, max_results)
128
+
129
+ print(f"📡 Fetching from YouTube API (cached: {cached_count}, need: {max_results})")
130
+
131
+ all_comments = []
132
+ next_page_token = None
133
+ comments_fetched = 0
134
+
135
+ # YouTube API max per request is 100
136
+ per_page = min(100, max_results)
137
+ page = 1
138
+
139
+ while comments_fetched < max_results:
140
+ # Build URL with pagination
141
+ url = f"https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId={video_id}&maxResults={per_page}&key={API_KEY}"
142
+ if next_page_token:
143
+ url += f"&pageToken={next_page_token}"
144
+
145
+ print(f"Fetching page {page}...", end=" ")
146
+ response = requests.get(url, timeout=30)
147
+ data = response.json()
148
+
149
+ if "error" in data:
150
+ error_msg = data['error'].get('message', 'Unknown error')
151
+ print(f"\n❌ API Error: {error_msg}")
152
+
153
+ if "quotaExceeded" in error_msg:
154
+ print("⚠️ API quota exceeded. Using cached comments if available.")
155
+ if all_comments:
156
+ save_comments_to_cache(video_id, all_comments)
157
+ return load_comments_from_cache(video_id, max_results)
158
+ elif "commentsDisabled" in error_msg:
159
+ print("Comments are disabled for this video.")
160
+ break
161
+
162
+ if "items" not in data or not data["items"]:
163
+ print("No more comments found")
164
+ break
165
+
166
+ # Extract comments from this batch
167
+ for item in data["items"]:
168
+ comment = item["snippet"]["topLevelComment"]["snippet"]["textDisplay"]
169
+ # Clean HTML entities
170
+ comment = re.sub(r'<.*?>', '', comment)
171
+ comment = comment.replace('&amp;', '&').replace('&lt;', '<').replace('&gt;', '>')
172
+ comment = comment.replace('&#39;', "'").replace('&quot;', '"')
173
+ all_comments.append(comment)
174
+ comments_fetched += 1
175
+
176
+ if comments_fetched >= max_results:
177
+ break
178
+
179
+ print(f"✅ Got {len(all_comments)} comments so far")
180
+
181
+ # Check if there are more pages
182
+ next_page_token = data.get("nextPageToken")
183
+ if not next_page_token:
184
+ print("No more pages available")
185
+ break
186
+
187
+ page += 1
188
+
189
+ # Small delay to avoid rate limiting
190
+ time.sleep(0.2)
191
+
192
+ # Save to cache for future use
193
+ if all_comments:
194
+ save_comments_to_cache(video_id, all_comments)
195
+
196
+ print(f"✅ Successfully fetched {len(all_comments)} comments from YouTube!")
197
+ return all_comments
198
+
199
+ except requests.exceptions.Timeout:
200
+ print("Request timeout. Try again with fewer comments.")
201
+ return []
202
+ except Exception as e:
203
+ print(f"Error fetching comments: {e}")
204
+ return []
205
+
206
+ # New function for lakhs of comments with resume support
207
+ def get_comments_batch(video_url, batch_size=500, offset=0):
208
+ """
209
+ Fetch comments in batches for lakhs of comments
210
+ Returns (comments, has_more, total_fetched)
211
+ """
212
+ try:
213
+ video_id = extract_video_id(video_url)
214
+ if not video_id:
215
+ return [], False, 0
216
+
217
+ init_cache_db()
218
+
219
+ # Try to get from cache first
220
+ cached = load_comments_from_cache(video_id, batch_size)
221
+ if len(cached) >= batch_size:
222
+ return cached[:batch_size], True, len(cached)
223
+
224
+ # Need to fetch more
225
+ all_comments = []
226
+ next_page_token = None
227
+ fetched = 0
228
+
229
+ url = f"https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId={video_id}&maxResults=100&key={API_KEY}"
230
+
231
+ # Skip to offset by paginating
232
+ pages_to_skip = offset // 100
233
+ for _ in range(pages_to_skip):
234
+ response = requests.get(url, timeout=30)
235
+ data = response.json()
236
+ next_page_token = data.get("nextPageToken")
237
+ if not next_page_token:
238
+ break
239
+ url = f"https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId={video_id}&maxResults=100&pageToken={next_page_token}&key={API_KEY}"
240
+
241
+ while fetched < batch_size:
242
+ response = requests.get(url, timeout=30)
243
+ data = response.json()
244
+
245
+ if "error" in data or "items" not in data:
246
+ break
247
+
248
+ for item in data["items"]:
249
+ comment = item["snippet"]["topLevelComment"]["snippet"]["textDisplay"]
250
+ comment = re.sub(r'<.*?>', '', comment)
251
+ all_comments.append(comment)
252
+ fetched += 1
253
+ if fetched >= batch_size:
254
+ break
255
+
256
+ next_page_token = data.get("nextPageToken")
257
+ if not next_page_token:
258
+ break
259
+
260
+ url = f"https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId={video_id}&maxResults=100&pageToken={next_page_token}&key={API_KEY}"
261
+ time.sleep(0.2)
262
+
263
+ if all_comments:
264
+ save_comments_to_cache(video_id, all_comments)
265
+
266
+ has_more = next_page_token is not None
267
+ return all_comments, has_more, len(all_comments)
268
+
269
+ except Exception as e:
270
+ print(f"Error in batch fetch: {e}")
271
+ return [], False, 0
main.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import List, Optional
5
+ from data.fetch_comments import get_comments_from_url, get_comments_batch, init_cache_db
6
+ from model.model_loader import (
7
+ predict_sentiment,
8
+ detect_sarcasm,
9
+ detect_emotion,
10
+ generate_summary,
11
+ extract_keywords,
12
+ process_comments_in_batches,
13
+ get_batch_stats
14
+ )
15
+ import time
16
+ from datetime import datetime
17
+ import asyncio
18
+ from threading import Thread
19
+ import os
20
+
21
+ app = FastAPI(title="YouTube Comment Analyzer API", version="2.0.0")
22
+
23
+ # Add CORS middleware
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ # Initialize cache DB on startup
33
+ @app.on_event("startup")
34
+ async def startup_event():
35
+ init_cache_db()
36
+ print("✅ Cache database initialized")
37
+
38
+ class SingleCommentRequest(BaseModel):
39
+ text: str
40
+
41
+ # Store background job status
42
+ analysis_jobs = {}
43
+
44
+ @app.get("/")
45
+ async def root():
46
+ return {
47
+ "message": "YouTube Comment Analyzer API",
48
+ "status": "running",
49
+ "version": "2.0.0",
50
+ "max_comments": "UNLIMITED (supports lakhs of comments with caching)",
51
+ "endpoints": {
52
+ "/analyze_youtube": "GET - Analyze YouTube video comments (up to 1000 quickly)",
53
+ "/analyze_large": "POST - Analyze lakhs of comments asynchronously",
54
+ "/job_status/{job_id}": "GET - Check background job status",
55
+ "/predict": "POST - Analyze single comment",
56
+ "/health": "GET - Check API health"
57
+ }
58
+ }
59
+
60
+ @app.get("/health")
61
+ async def health_check():
62
+ return {
63
+ "status": "healthy",
64
+ "timestamp": datetime.now().isoformat(),
65
+ "models_loaded": True,
66
+ "cache_initialized": True
67
+ }
68
+
69
+ @app.get("/analyze_youtube")
70
+ async def analyze_youtube(url: str, limit: int = 500):
71
+ """
72
+ Analyze YouTube video comments
73
+ NOW SUPPORTS UP TO 10000+ COMMENTS (removed 1000 cap)
74
+ """
75
+ try:
76
+ # REMOVED THE 1000 CAP - Now supports more!
77
+ # if limit > 1000:
78
+ # limit = 1000
79
+
80
+ # Cap at 10000 for reasonable response time, but cache handles more
81
+ if limit > 10000:
82
+ print(f"⚠️ Large request: {limit} comments. This may take several minutes.")
83
+
84
+ print(f"\n{'='*60}")
85
+ print(f"Analyzing YouTube URL: {url}")
86
+ print(f"Comment limit: {limit}")
87
+ print(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
88
+ print(f"{'='*60}")
89
+
90
+ start_time = time.time()
91
+
92
+ # Fetch real comments from YouTube (now with caching)
93
+ comments = get_comments_from_url(url, limit)
94
+
95
+ if not comments:
96
+ return {
97
+ "total_comments": 0,
98
+ "summary": "No comments found for this video.",
99
+ "keywords": [],
100
+ "stats": {"positive": 0, "neutral": 0, "negative": 0},
101
+ "sentiment_score": 0,
102
+ "results": [],
103
+ "timestamp": datetime.now().isoformat(),
104
+ "processing_time": round(time.time() - start_time, 2)
105
+ }
106
+
107
+ print(f"Processing {len(comments)} comments with ML models...")
108
+
109
+ # Process in batches for better performance
110
+ all_results = []
111
+ stats = {"positive": 0, "neutral": 0, "negative": 0}
112
+
113
+ # Use batch processing
114
+ for batch_results in process_comments_in_batches(comments, batch_size=100):
115
+ for result in batch_results:
116
+ all_results.append(result)
117
+ if result["sentiment"] == "POSITIVE":
118
+ stats["positive"] += 1
119
+ elif result["sentiment"] == "NEGATIVE":
120
+ stats["negative"] += 1
121
+ else:
122
+ stats["neutral"] += 1
123
+
124
+ # Generate summary and keywords
125
+ print("Generating AI summary...")
126
+ summary = generate_summary(comments[:200]) # Use more comments for better summary
127
+
128
+ print("Extracting keywords...")
129
+ keywords = extract_keywords(comments[:300]) # Use more comments for better keywords
130
+
131
+ # Calculate sentiment score
132
+ total = stats["positive"] + stats["neutral"] + stats["negative"]
133
+ sentiment_score = ((stats["positive"] - stats["negative"]) / total * 100) if total > 0 else 0
134
+
135
+ processing_time = round(time.time() - start_time, 2)
136
+
137
+ response_data = {
138
+ "total_comments": len(all_results),
139
+ "summary": summary,
140
+ "keywords": keywords[:20], # More keywords
141
+ "stats": stats,
142
+ "sentiment_score": round(sentiment_score, 1),
143
+ "results": all_results,
144
+ "timestamp": datetime.now().isoformat(),
145
+ "processing_time": processing_time
146
+ }
147
+
148
+ print(f"\n✅ Analysis Complete!")
149
+ print(f" Total Comments: {len(all_results)}")
150
+ print(f" Positive: {stats['positive']} ({stats['positive']/total*100:.1f}%)")
151
+ print(f" Neutral: {stats['neutral']} ({stats['neutral']/total*100:.1f}%)")
152
+ print(f" Negative: {stats['negative']} ({stats['negative']/total*100:.1f}%)")
153
+ print(f" Sentiment Score: {sentiment_score:.1f}%")
154
+ print(f" Processing Time: {processing_time} seconds")
155
+ print(f"{'='*60}\n")
156
+
157
+ return response_data
158
+
159
+ except Exception as e:
160
+ print(f"Error in analyze_youtube: {str(e)}")
161
+ import traceback
162
+ traceback.print_exc()
163
+ raise HTTPException(status_code=500, detail=str(e))
164
+
165
+ @app.post("/analyze_large")
166
+ async def analyze_large_scale(
167
+ url: str,
168
+ max_comments: int = 100000, # Now supports lakhs!
169
+ background_tasks: BackgroundTasks = None
170
+ ):
171
+ """
172
+ Analyze up to 100,000+ comments asynchronously
173
+ Returns job_id to check status
174
+ """
175
+ import uuid
176
+ job_id = str(uuid.uuid4())
177
+
178
+ analysis_jobs[job_id] = {
179
+ "status": "pending",
180
+ "progress": 0,
181
+ "total": max_comments,
182
+ "started_at": datetime.now().isoformat()
183
+ }
184
+
185
+ def run_large_analysis():
186
+ try:
187
+ analysis_jobs[job_id]["status"] = "running"
188
+
189
+ # Fetch in batches
190
+ all_comments = []
191
+ offset = 0
192
+ batch_size = 1000
193
+
194
+ while len(all_comments) < max_comments:
195
+ batch, has_more, fetched = get_comments_batch(url, batch_size, offset)
196
+ if not batch:
197
+ break
198
+ all_comments.extend(batch)
199
+ offset += fetched
200
+ analysis_jobs[job_id]["progress"] = len(all_comments)
201
+
202
+ if not has_more:
203
+ break
204
+
205
+ # Process in batches
206
+ all_results = []
207
+ stats = {"positive": 0, "neutral": 0, "negative": 0}
208
+
209
+ for batch_results in process_comments_in_batches(all_comments, batch_size=200):
210
+ for result in batch_results:
211
+ all_results.append(result)
212
+ if result["sentiment"] == "POSITIVE":
213
+ stats["positive"] += 1
214
+ elif result["sentiment"] == "NEGATIVE":
215
+ stats["negative"] += 1
216
+ else:
217
+ stats["neutral"] += 1
218
+
219
+ analysis_jobs[job_id]["progress"] = len(all_results)
220
+
221
+ analysis_jobs[job_id]["results"] = {
222
+ "total_comments": len(all_results),
223
+ "stats": stats,
224
+ "sentiment_score": ((stats["positive"] - stats["negative"]) / len(all_results) * 100) if len(all_results) > 0 else 0,
225
+ "summary": generate_summary(all_comments[:200]),
226
+ "keywords": extract_keywords(all_comments[:300])
227
+ }
228
+ analysis_jobs[job_id]["status"] = "completed"
229
+ analysis_jobs[job_id]["completed_at"] = datetime.now().isoformat()
230
+
231
+ except Exception as e:
232
+ analysis_jobs[job_id]["status"] = "failed"
233
+ analysis_jobs[job_id]["error"] = str(e)
234
+
235
+ # Run in background
236
+ thread = Thread(target=run_large_analysis)
237
+ thread.start()
238
+
239
+ return {
240
+ "job_id": job_id,
241
+ "status": "started",
242
+ "message": f"Analysis started for up to {max_comments} comments. Use /job_status/{job_id} to check progress"
243
+ }
244
+
245
+ @app.get("/job_status/{job_id}")
246
+ async def get_job_status(job_id: str):
247
+ """Check status of large analysis job"""
248
+ if job_id not in analysis_jobs:
249
+ raise HTTPException(status_code=404, detail="Job not found")
250
+
251
+ job = analysis_jobs[job_id]
252
+ response = {
253
+ "job_id": job_id,
254
+ "status": job["status"],
255
+ "progress": job.get("progress", 0),
256
+ "total": job.get("total", 0)
257
+ }
258
+
259
+ if job["status"] == "completed":
260
+ response["results"] = job.get("results")
261
+ response["completed_at"] = job.get("completed_at")
262
+ elif job["status"] == "failed":
263
+ response["error"] = job.get("error")
264
+
265
+ return response
266
+
267
+ @app.post("/predict")
268
+ async def predict_sentiment_endpoint(request: SingleCommentRequest):
269
+ """Analyze sentiment of a single comment"""
270
+ try:
271
+ sentiment = predict_sentiment(request.text)
272
+ sarcasm = detect_sarcasm(request.text)
273
+ emotion = detect_emotion(request.text)
274
+
275
+ return {
276
+ "text": request.text,
277
+ "sentiment": sentiment,
278
+ "sarcasm": sarcasm,
279
+ "emotion": emotion,
280
+ "timestamp": datetime.now().isoformat()
281
+ }
282
+ except Exception as e:
283
+ raise HTTPException(status_code=500, detail=str(e))
284
+
285
+ if __name__ == "__main__":
286
+ import uvicorn
287
+ # Hugging Face Spaces uses port 7860
288
+ port = int(os.environ.get("PORT", 7860))
289
+ print("\n" + "="*60)
290
+ print("🚀 YouTube Comment Analyzer API v2.0 - Hugging Face Edition")
291
+ print("="*60)
292
+ print(f"Server starting on http://0.0.0.0:{port}")
293
+ print("✅ Supports 100,000+ comments with caching")
294
+ print("✅ SQLite cache for faster subsequent analysis")
295
+ print("="*60 + "\n")
296
+ uvicorn.run(app, host="0.0.0.0", port=port)
297
+
298
+
model/__pycache__/model_loader.cpython-311.pyc ADDED
Binary file (13.6 kB). View file
 
model/model_loader.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import pipeline
2
+ from keybert import KeyBERT
3
+ import torch
4
+ import re
5
+ from collections import Counter
6
+ import numpy as np
7
+
8
+ # Check if GPU is available
9
+ device = 0 if torch.cuda.is_available() else -1
10
+ print(f"Using device: {'GPU' if torch.cuda.is_available() else 'CPU'}")
11
+
12
+ # Load sentiment analysis model
13
+ print("Loading sentiment analysis model...")
14
+ sentiment_model = pipeline(
15
+ "sentiment-analysis",
16
+ model="cardiffnlp/twitter-roberta-base-sentiment-latest",
17
+ device=device,
18
+ truncation=True,
19
+ max_length=512
20
+ )
21
+
22
+ # Load sarcasm detection model
23
+ print("Loading sarcasm detection model...")
24
+ sarcasm_model = pipeline(
25
+ "text-classification",
26
+ model="cardiffnlp/twitter-roberta-base-irony",
27
+ device=device,
28
+ truncation=True,
29
+ max_length=512
30
+ )
31
+
32
+ # Load emotion detection model
33
+ print("Loading emotion detection model...")
34
+ emotion_model = pipeline(
35
+ "text-classification",
36
+ model="j-hartmann/emotion-english-distilroberta-base",
37
+ device=device,
38
+ truncation=True,
39
+ max_length=512
40
+ )
41
+
42
+ # Load summarizer
43
+ print("Loading summarizer model...")
44
+ summarizer = pipeline(
45
+ "summarization",
46
+ model="facebook/bart-large-cnn",
47
+ device=device,
48
+ truncation=True,
49
+ max_length=1024
50
+ )
51
+
52
+ # Load keyword extractor
53
+ print("Loading keyword extractor...")
54
+ kw_model = KeyBERT()
55
+
56
+ print("All models loaded successfully!")
57
+
58
+ # Positive and negative word lists for fallback
59
+ POSITIVE_WORDS = {
60
+ 'love', '❤️', '💕', '💓', '💗', '💖', '💘', '💝',
61
+ 'great', 'amazing', 'awesome', 'fantastic', 'wonderful',
62
+ 'beautiful', 'perfect', 'excellent', 'brilliant',
63
+ 'fan', 'favorite', 'favourite', 'best', 'good', 'nice',
64
+ 'like', 'enjoy', 'appreciate', 'thank', 'thanks', 'legend'
65
+ }
66
+
67
+ NEGATIVE_WORDS = {
68
+ 'hate', 'bad', 'terrible', 'awful', 'horrible', 'sucks',
69
+ 'dislike', 'worst', 'poor', 'disappointing', 'waste',
70
+ 'boring', 'useless', 'trash', 'garbage', 'cringe',
71
+ 'overrated', 'hated', 'annoying', 'stupid', 'dumb'
72
+ }
73
+
74
+ def safe_truncate(text, max_length=512):
75
+ """Safely truncate text to max_length characters"""
76
+ if not text:
77
+ return ""
78
+ if len(text) > max_length:
79
+ return text[:max_length]
80
+ return text
81
+
82
+ def clean_text(text: str) -> tuple:
83
+ """Clean and normalize text"""
84
+ if not text:
85
+ return "", ""
86
+ text = ' '.join(text.split())
87
+ text_lower = text.lower()
88
+ return text, text_lower
89
+
90
+ def predict_sentiment(text: str) -> str:
91
+ """
92
+ FIXED: Unbiased sentiment prediction
93
+ Let the model decide without forcing positive/neutral
94
+ """
95
+ try:
96
+ if not text or len(text.strip()) < 2:
97
+ return "NEUTRAL"
98
+
99
+ # Clean and truncate
100
+ text = safe_truncate(text, 512)
101
+ original_text = text
102
+ text, text_lower = clean_text(text)
103
+
104
+ # Get model prediction (primary source)
105
+ try:
106
+ result = sentiment_model(text)[0]
107
+ model_label = result['label']
108
+ model_score = result['score']
109
+
110
+ # Map model labels to our categories
111
+ # LABEL_0 = Negative, LABEL_1 = Neutral, LABEL_2 = Positive
112
+ if model_label == "LABEL_0":
113
+ return "NEGATIVE"
114
+ elif model_label == "LABEL_2":
115
+ return "POSITIVE"
116
+ elif model_label == "LABEL_1":
117
+ # Only return neutral if confidence is high
118
+ if model_score > 0.8:
119
+ return "NEUTRAL"
120
+ # Otherwise, check keywords to decide
121
+ pass
122
+
123
+ except Exception as model_err:
124
+ print(f"Model error: {model_err}")
125
+ # Fall through to keyword analysis
126
+
127
+ # Keyword-based analysis (fallback only)
128
+ pos_count = sum(1 for word in POSITIVE_WORDS if word in text_lower)
129
+ neg_count = sum(1 for word in NEGATIVE_WORDS if word in text_lower)
130
+
131
+ # Simple majority rule for keywords
132
+ if pos_count > neg_count and pos_count > 0:
133
+ return "POSITIVE"
134
+ elif neg_count > pos_count and neg_count > 0:
135
+ return "NEGATIVE"
136
+
137
+ # Default to neutral only if absolutely no signal
138
+ return "NEUTRAL"
139
+
140
+ except Exception as e:
141
+ print(f"Error in sentiment analysis: {e}")
142
+ return "NEUTRAL"
143
+
144
+ def detect_sarcasm(text: str) -> str:
145
+ """Detect sarcasm in comment"""
146
+ try:
147
+ if not text:
148
+ return "NO"
149
+
150
+ text = safe_truncate(text, 512)
151
+ if len(text.strip()) < 3:
152
+ return "NO"
153
+
154
+ result = sarcasm_model(text)[0]
155
+ # LABEL_1 = sarcastic
156
+ return "YES" if result['label'] == "LABEL_1" and result['score'] > 0.55 else "NO"
157
+ except Exception as e:
158
+ return "NO"
159
+
160
+ def detect_emotion(text: str) -> str:
161
+ """Detect emotion in comment"""
162
+ try:
163
+ if not text:
164
+ return "neutral"
165
+
166
+ # Emoji-based fast detection
167
+ if '😭' in text or '😢' in text:
168
+ return "sadness"
169
+ elif '😊' in text or '😍' in text or '🥰' in text:
170
+ return "joy"
171
+ elif '😂' in text or '🤣' in text:
172
+ return "amusement"
173
+ elif '❤️' in text or '💕' in text:
174
+ return "love"
175
+ elif '🎉' in text or '🎊' in text:
176
+ return "excitement"
177
+ elif '😠' in text or '🤬' in text:
178
+ return "anger"
179
+ elif '😨' in text or '😱' in text:
180
+ return "fear"
181
+
182
+ # Model-based detection
183
+ text = safe_truncate(text, 512)
184
+ if len(text.strip()) < 3:
185
+ return "neutral"
186
+
187
+ result = emotion_model(text)[0]
188
+ return result['label']
189
+ except Exception as e:
190
+ return "neutral"
191
+
192
+ def generate_summary(texts: list) -> str:
193
+ """Generate summary of all comments"""
194
+ try:
195
+ if not texts:
196
+ return "No comments to summarize"
197
+
198
+ sample_size = min(100, len(texts))
199
+ combined = " ".join(texts[:sample_size])
200
+ combined = safe_truncate(combined, 1024)
201
+
202
+ if len(combined) < 50:
203
+ return "Not enough comments to generate summary"
204
+
205
+ summary = summarizer(combined, max_length=150, min_length=40, do_sample=False)
206
+ return summary[0]['summary_text']
207
+ except Exception as e:
208
+ print(f"Error in summary generation: {e}")
209
+ return "Summary generation failed"
210
+
211
+ def extract_keywords(texts: list, top_n=15):
212
+ """Extract keywords from comments"""
213
+ try:
214
+ if not texts:
215
+ return []
216
+
217
+ sample_size = min(200, len(texts))
218
+ combined = " ".join(texts[:sample_size])
219
+ combined = safe_truncate(combined, 2000)
220
+
221
+ if len(combined) < 20:
222
+ return []
223
+
224
+ keywords = kw_model.extract_keywords(
225
+ combined,
226
+ keyphrase_ngram_range=(1, 2),
227
+ stop_words='english',
228
+ top_n=top_n
229
+ )
230
+ return [kw[0] for kw in keywords if kw and kw[0]]
231
+ except Exception as e:
232
+ print(f"Error in keyword extraction: {e}")
233
+ # Fallback: simple word frequency
234
+ words = combined.lower().split()
235
+ stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were',
236
+ 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'without', 'i',
237
+ 'you', 'he', 'she', 'it', 'we', 'they', 'this', 'that', 'these', 'those'}
238
+ word_freq = {}
239
+ for word in words:
240
+ word = word.strip('.,!?;:()[]{}"\'')
241
+ if len(word) > 2 and word not in stop_words and not word.isdigit():
242
+ word_freq[word] = word_freq.get(word, 0) + 1
243
+ sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:top_n]
244
+ return [word for word, count in sorted_words]
245
+
246
+ def process_comments_in_batches(comments, batch_size=100):
247
+ """Process comments in batches"""
248
+ total = len(comments)
249
+
250
+ if total == 0:
251
+ return
252
+
253
+ print(f"Processing {total} comments in batches of {batch_size}...")
254
+
255
+ for i in range(0, total, batch_size):
256
+ batch = comments[i:i+batch_size]
257
+ batch_results = []
258
+
259
+ for comment in batch:
260
+ try:
261
+ if not comment or len(comment.strip()) < 2:
262
+ batch_results.append({
263
+ "text": comment if comment else "",
264
+ "sentiment": "NEUTRAL",
265
+ "sarcasm": "NO",
266
+ "emotion": "neutral"
267
+ })
268
+ continue
269
+
270
+ sentiment = predict_sentiment(comment)
271
+ sarcasm = detect_sarcasm(comment)
272
+ emotion = detect_emotion(comment)
273
+
274
+ batch_results.append({
275
+ "text": comment,
276
+ "sentiment": sentiment,
277
+ "sarcasm": sarcasm,
278
+ "emotion": emotion
279
+ })
280
+ except Exception as e:
281
+ batch_results.append({
282
+ "text": comment if comment else "",
283
+ "sentiment": "NEUTRAL",
284
+ "sarcasm": "NO",
285
+ "emotion": "unknown"
286
+ })
287
+
288
+ yield batch_results
289
+
290
+ if (i // batch_size + 1) % 10 == 0 or (i + batch_size) >= total:
291
+ processed = min(i + batch_size, total)
292
+ print(f" Processed batch {i//batch_size + 1}/{(total + batch_size - 1)//batch_size} ({processed}/{total} comments)")
293
+
294
+ def get_batch_stats(results_batches):
295
+ """Aggregate statistics from batch results"""
296
+ stats = {"positive": 0, "neutral": 0, "negative": 0}
297
+ all_results = []
298
+
299
+ for batch in results_batches:
300
+ for item in batch:
301
+ all_results.append(item)
302
+ if item["sentiment"] == "POSITIVE":
303
+ stats["positive"] += 1
304
+ elif item["sentiment"] == "NEGATIVE":
305
+ stats["negative"] += 1
306
+ else:
307
+ stats["neutral"] += 1
308
+
309
+ return stats, all_results
310
+
311
+ def get_sentiment_score(stats):
312
+ """Calculate overall sentiment score"""
313
+ total = stats["positive"] + stats["neutral"] + stats["negative"]
314
+ if total == 0:
315
+ return 0
316
+ return ((stats["positive"] - stats["negative"]) / total) * 100
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.27.0
3
+ transformers==4.36.0
4
+ torch==2.1.0
5
+ keybert==0.8.0
6
+ requests==2.31.0
7
+ yt-dlp==2023.12.30
8
+ python-multipart==0.0.6
9
+ sentencepiece==0.1.99
10
+ protobuf==3.20.3
11
+ numpy==1.24.3
12
+ sentence-transformers==2.2.2
13
+ aiofiles==23.2.1
14
+ huggingface-hub==0.19.4
test_api.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+
3
+ API_KEY = "AIzaSyBwwLnHXKmL5VQhkKzumCS5r1Cbi3HdUro"
4
+ video_id = "dQw4w9WgXcQ" # Rick Astley - has many comments
5
+
6
+ print("="*50)
7
+ print("Testing YouTube API Key")
8
+ print("="*50)
9
+ print(f"API Key: {API_KEY[:10]}...{API_KEY[-10:]}")
10
+ print(f"Video ID: {video_id}")
11
+ print()
12
+
13
+ # Test video info endpoint
14
+ url = f"https://www.googleapis.com/youtube/v3/videos?part=statistics&id={video_id}&key={API_KEY}"
15
+ response = requests.get(url)
16
+ data = response.json()
17
+
18
+ if "error" in data:
19
+ print("[ERROR] API Error:")
20
+ print(f" Message: {data['error']['message']}")
21
+ print(f" Code: {data['error']['code']}")
22
+ print(f" Reason: {data['error']['errors'][0]['reason']}")
23
+ print()
24
+ print("Possible solutions:")
25
+ print("1. Enable YouTube Data API v3 in Google Cloud Console")
26
+ print("2. Check if API key is correct")
27
+ print("3. Make sure billing is enabled (even for free tier)")
28
+ else:
29
+ print("[SUCCESS] API Key is valid!")
30
+ if 'items' in data and len(data['items']) > 0:
31
+ print(f" Video found!")
32
+ comment_count = data['items'][0]['statistics'].get('commentCount', 0)
33
+ print(f" Comment count: {comment_count}")
34
+
35
+ # Test fetching comments
36
+ print()
37
+ print("Testing comment fetch...")
38
+ comments_url = f"https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId={video_id}&maxResults=5&key={API_KEY}"
39
+ comments_response = requests.get(comments_url)
40
+ comments_data = comments_response.json()
41
+
42
+ if "items" in comments_data:
43
+ print(f"[SUCCESS] Successfully fetched {len(comments_data['items'])} comments!")
44
+ if len(comments_data['items']) > 0:
45
+ first_comment = comments_data['items'][0]['snippet']['topLevelComment']['snippet']['textDisplay']
46
+ print(f" First comment: {first_comment[:100]}...")
47
+ else:
48
+ print("[WARNING] Could not fetch comments:")
49
+ if "error" in comments_data:
50
+ print(f" {comments_data['error'].get('message', 'Unknown error')}")
51
+ else:
52
+ print("[ERROR] Video not found")
53
+
54
+ print("="*50)