Spaces:
Sleeping
Sleeping
File size: 5,214 Bytes
f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """Stage 2: rank candidate videos by the sentiment of their comments.
Comments are fetched via the **YouTube Data API v3** (commentThreads) using an API key,
then scored with the BERT classifier ``OmarMedhat7/youtube-sentiment-analysis-model``
running inside the Space. The video with the highest positive share wins. Any video whose
comments are disabled or fail to fetch simply scores 0 and never aborts the ranking.
"""
from __future__ import annotations
import json
import urllib.error
import urllib.parse
import urllib.request
from functools import lru_cache
SENTIMENT_MODEL = "OmarMedhat7/youtube-sentiment-analysis-model"
COMMENTS_API = "https://www.googleapis.com/youtube/v3/commentThreads"
MAX_COMMENTS = 200 # cap per video to bound time/quota (commentThreads = 1 unit/100)
_POSITIVE = {"positive", "pos", "label_2", "label_1"}
_NEGATIVE = {"negative", "neg", "label_0"}
class SentimentError(RuntimeError):
"""Raised for API-key / quota problems that should surface to the user."""
@lru_cache(maxsize=1)
def _classifier():
from transformers import pipeline
return pipeline(
"text-classification",
model=SENTIMENT_MODEL,
truncation=True,
max_length=256,
top_k=None,
)
def _polarity(scores) -> float:
val = 0.0
for s in scores:
label = str(s["label"]).strip().lower()
if label in _POSITIVE:
val += s["score"]
elif label in _NEGATIVE:
val -= s["score"]
return val
def _fetch_comments(video_id: str, api_key: str, cap: int = MAX_COMMENTS) -> list[str]:
"""Fetch up to ``cap`` top-relevance comments via the YouTube Data API.
Returns [] if comments are disabled. Raises SentimentError on auth/quota failures
(which apply to every video, so the caller should stop).
"""
comments: list[str] = []
page = None
while len(comments) < cap:
params = {
"part": "snippet",
"videoId": video_id,
"maxResults": "100",
"order": "relevance",
"textFormat": "plainText",
"key": api_key,
}
if page:
params["pageToken"] = page
url = COMMENTS_API + "?" + urllib.parse.urlencode(params)
try:
with urllib.request.urlopen(url, timeout=30) as resp:
data = json.load(resp)
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", "ignore")
if exc.code == 403 and "commentsDisabled" in body:
return []
if exc.code in (400, 403) and ("keyInvalid" in body or "quota" in body.lower()
or "forbidden" in body.lower()):
raise SentimentError(
f"YouTube Data API rejected the key (HTTP {exc.code}). Check the key "
f"and that YouTube Data API v3 is enabled / has quota. {body[:160]}")
# video-specific error (e.g. not found): treat as no comments
return comments
except Exception:
return comments
for item in data.get("items", []):
sn = item.get("snippet", {}).get("topLevelComment", {}).get("snippet", {})
text = sn.get("textDisplay") or sn.get("textOriginal")
if text:
comments.append(text.strip())
page = data.get("nextPageToken")
if not page:
break
return comments[:cap]
def rank_by_sentiment(videos: list[dict], api_key: str,
progress=None) -> tuple[dict, list[dict]]:
"""Score each video by comment sentiment; return ``(best_video, scored)``.
``scored`` mirrors ``videos`` with ``positive_share`` (0..1), ``n_comments``, ``note``
and is sorted by positive_share desc (search order as tie-break / fallback).
"""
if not videos:
raise ValueError("No videos to rank.")
if not api_key:
raise SentimentError("A YouTube Data API key is required to fetch comments.")
clf = _classifier()
scored: list[dict] = []
for i, v in enumerate(videos):
if progress:
progress((i + 1) / len(videos), desc=f"Sentiment {i + 1}/{len(videos)}")
item = dict(v)
try:
comments = _fetch_comments(v["video_id"], api_key)
if comments:
results = clf(comments)
pols = [_polarity(r) for r in results]
positives = sum(1 for p in pols if p > 0)
item["positive_share"] = positives / len(pols)
item["n_comments"] = len(comments)
item["note"] = ""
else:
item["positive_share"] = 0.0
item["n_comments"] = 0
item["note"] = "no comments"
except SentimentError:
raise # key/quota problem affects all videos
except Exception as exc:
item["positive_share"] = 0.0
item["n_comments"] = 0
item["note"] = f"fetch failed: {type(exc).__name__}"
item["search_rank"] = i
scored.append(item)
scored.sort(key=lambda d: (-d["positive_share"], d["search_rank"]))
return scored[0], scored
|