Spaces:
Paused
Paused
File size: 13,693 Bytes
b5496dd cd24f02 b5496dd cf93474 7ffa586 cf93474 7ffa586 b5496dd cf93474 7ffa586 cf93474 7ffa586 b5496dd 7ffa586 7b70bf3 7ffa586 7b70bf3 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd cf93474 b6370ad 7ffa586 b5496dd cf93474 b5496dd 7ffa586 cf93474 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd cf93474 7ffa586 b5496dd 7ffa586 b5496dd cf93474 7ffa586 b5496dd e852ab6 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd cf93474 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd e852ab6 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd f0091fa 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd cf93474 b5496dd cf93474 b5496dd cf93474 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 b5496dd 7ffa586 | 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | import os
import re
import streamlit as st
import pandas as pd
import plotly.express as px
from googleapiclient.discovery import build
from transformers import pipeline
from typing import Union
# Set environment variables for cache directories
# This helps manage where Streamlit and Hugging Face cache files are stored,
# especially useful in environments like Hugging Face Spaces.
os.environ['STREAMLIT_SERVER_BROWSER_CACHE_PATH'] = '/tmp/.streamlit_browser_cache'
os.environ['STREAMLIT_SERVER_LIB_CACHE_PATH'] = '/tmp/.streamlit_lib_cache'
os.environ['STREAMLIT_SERVER_STATIC_CACHE_PATH'] = '/tmp/.streamlit_static_cache'
os.environ['MPLCONFIGDIR'] = '/tmp/.config/matplotlib'
# IMPORTANT: Changing this cache directory name (e.g., to _v5, _v6)
# will force Hugging Face to re-download all models, effectively clearing
# any potentially corrupted cached models.
# Incrementing to _v6 to ensure a completely fresh start for the cache.
os.environ['HUGGINGFACE_HUB_CACHE'] = '/tmp/huggingface_models_cache_v6'
# Suppress TensorFlow OneDNN optimization warnings if TensorFlow is installed
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
# Configure Streamlit page settings
st.set_page_config(
layout="wide",
page_title="YouTube Comment Sentiment Analyzer",
page_icon="https://upload.wikimedia.org/wikipedia/commons/4/47/Google_pixel_logo.svg"
)
# --- Debugger visualization (for environment variables) ---
st.sidebar.subheader("Environment Variables (for debugging):")
st.sidebar.write(f"HF_HOME: {os.environ.get('HF_HOME')}")
st.sidebar.write(f"TRANSFORMERS_CACHE: {os.environ.get('TRANSFORMERS_CACHE')}")
st.sidebar.write(f"HF_HUB_CACHE: {os.environ.get('HUGGINGFACE_HUB_CACHE')}")
st.sidebar.write(f"YOUTUBE_API_KEY present: {bool(os.environ.get('YOUTUBE_API_KEY'))}")
# --- End debugger ---
# Regex to extract YouTube video ID from various URL formats
YOUTUBE_URL_REGEX = re.compile(
r"(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^\"&?\/\s]{11})"
)
def get_video_id(youtube_url: str) -> Union[str, None]:
"""
Extracts the YouTube video ID from a given URL.
Args:
youtube_url (str): The URL of the YouTube video.
Returns:
Union[str, None]: The 11-character video ID if found, otherwise None.
"""
match = YOUTUBE_URL_REGEX.search(youtube_url)
return match.group(1) if match else None
def fetch_comments(video_id: str, api_key: str, max_comments: int = 500) -> list[str]:
"""
Fetches comments for a given YouTube video ID using the YouTube Data API.
Args:
video_id (str): The ID of the YouTube video.
api_key (str): Your Google/YouTube Data API key.
max_comments (int): The maximum number of comments to fetch.
Returns:
list[str]: A list of fetched comment texts.
"""
try:
# Initialize YouTube API service client
youtube = build("youtube", "v3", developerKey=api_key)
comments = []
next_page_token = None
fetched_count = 0
# Loop to fetch comments page by page until max_comments is reached or no more pages
while fetched_count < max_comments:
request = youtube.commentThreads().list(
part="snippet",
videoId=video_id,
maxResults=100, # Max results per page allowed by API
pageToken=next_page_token,
textFormat="plainText"
)
response = request.execute()
# Extract comment text from the response
for item in response.get("items", []):
comment_text = item["snippet"]["topLevelComment"]["snippet"]["textDisplay"]
comments.append(comment_text)
fetched_count += 1
if fetched_count >= max_comments:
break
# Get token for the next page of comments
next_page_token = response.get("nextPageToken")
if not next_page_token:
break # No more pages
return comments
except Exception as e:
# Log the error for debugging in the console/logs
print(f"Error fetching comments: {e}")
# Display user-friendly error messages in Streamlit
st.error(f"An error occurred while fetching comments: {e}")
if "disabled comments" in str(e).lower():
st.info("Comments might be disabled for this video, or it's a Live Chat replay.")
elif "quota" in str(e).lower():
st.info("You might have hit your YouTube Data API daily quota.")
return []
# Sentiment analysis model name. This is the standard DistilBERT SST-2 finetuned model.
# If you intended to use the 'tiny' version, change this to "sshleifer/tiny-distilbert-base-uncased-finetuned-sst-2-english"
SENTIMENT_MODEL_NAME = "distilbert-base-uncased-finetuned-sst-2-english"
# Using the explicitly set environment for my cache directory.
# In case of errors, fallback to a default /tmp path.
HF_CACHE_DIR = os.environ.get('HUGGINGFACE_HUB_CACHE', '/tmp/huggingface_models_cache')
@st.cache_resource
def load_sentiment_model():
"""
Loads the sentiment analysis model using the Hugging Face transformers pipeline.
This function is cached by Streamlit to avoid re-loading the model on every rerun.
"""
try:
# Initialize the sentiment analysis pipeline
sentiment_pipeline = pipeline(
"sentiment-analysis",
model=SENTIMENT_MODEL_NAME,
tokenizer=SENTIMENT_MODEL_NAME,
top_k=None, # Get scores for all labels (POSITIVE, NEGATIVE)
# The HUGGINGFACE_HUB_CACHE environment variable is automatically respected by pipeline
# Removed force_download and local_files_only to simplify and rely on cache path change.
)
return sentiment_pipeline
except Exception as e:
# Provide detailed error messages to the user
st.error(f"Error loading sentiment model: {e}")
st.info(f"The app attempted to load model: '{SENTIMENT_MODEL_NAME}'.")
st.info(f"Please ensure the model '{SENTIMENT_MODEL_NAME}' is available and compatible.")
st.info(f"The model was attempted to be cached to: `{HF_CACHE_DIR}`.")
st.info("If the issue persists, consider these steps:")
st.info("1. **Check your internet connection** to ensure a stable download.")
st.info(f"2. **Force a fresh download** by changing the `HUGGINGFACE_HUB_CACHE` env var in your Space settings or directly in the `streamlit_app.py` file (e.g., to `/tmp/huggingface_models_cache_v7`).")
st.info("3. **Verify model availability** on Hugging Face Hub: `https://huggingface.co/{SENTIMENT_MODEL_NAME}`.")
st.stop() # Stop the Streamlit app execution gracefully if the model can't load
def analyze_sentiments(comments: list[str], sentiment_pipeline) -> pd.DataFrame:
"""
Analyzes the sentiment of a list of comments using the provided sentiment pipeline.
Args:
comments (list[str]): A list of comment texts.
sentiment_pipeline: The loaded Hugging Face sentiment analysis pipeline.
Returns:
pd.DataFrame: A DataFrame containing comments, their predicted sentiment, and score.
"""
if not comments:
return pd.DataFrame()
comment_data = []
for comment_text in comments:
# Skip empty or whitespace-only comments
if not comment_text.strip():
continue
sentiment_results = sentiment_pipeline(comment_text)
# Extract the main sentiment (POSITIVE/NEGATIVE) and its score
if sentiment_results and sentiment_results[0]:
# The model returns a list of dictionaries, usually sorted by score.
# We take the first one as the most confident prediction.
main_sentiment = sentiment_results[0][0]['label']
score = sentiment_results[0][0]['score']
comment_data.append({
"Comment": comment_text,
"Sentiment": main_sentiment,
"Score": round(score, 4)
})
return pd.DataFrame(comment_data)
def plot_sentiment_distribution(sentiment_df: pd.DataFrame):
"""
Plots the distribution of sentiments using a bar chart.
Args:
sentiment_df (pd.DataFrame): DataFrame containing sentiment analysis results.
"""
if sentiment_df.empty:
st.warning("No sentiment data to plot distribution.")
return
# Calculate counts for each sentiment label
sentiment_counts = sentiment_df['Sentiment'].value_counts().reset_index()
sentiment_counts.columns = ['Sentiment', 'Count']
# Define colors for the plot (Distilbert-base-uncased-finetuned-sst-2-english is binary)
color_map = {'POSITIVE': 'green', 'NEGATIVE': 'red'}
fig = px.bar(
sentiment_counts,
x='Sentiment',
y='Count',
title='Distribution of Comment Sentiments',
color='Sentiment',
color_discrete_map=color_map,
text='Count' # Display count value on top of bars
)
# Update layout for better readability
fig.update_traces(texttemplate='%{text}', textposition='outside')
fig.update_layout(xaxis_title="Sentiment", yaxis_title="Number of Comments")
st.plotly_chart(fig, use_container_width=True)
def display_sentiment_scores(sentiment_df: pd.DataFrame):
"""
Displays individual comment sentiments in a Streamlit DataFrame.
Args:
sentiment_df (pd.DataFrame): DataFrame containing sentiment analysis results.
"""
if sentiment_df.empty:
st.info("No comments available to display individual sentiment scores.")
return
st.subheader("Individual Comment Sentiments:")
st.dataframe(sentiment_df, use_container_width=True)
# --- Main Application Logic ---
YOUTUBE_API_KEY = os.environ.get("YOUTUBE_API_KEY")
st.title("π₯ (Phoenix) YouTube Comment Sentiment Checker")
st.markdown("Have you been attacked on YouTube? Report this by entering the YouTube video URL to fetch its comments and analyze their sentiment (Positive/Negative).")
# Check if API key is set
if not YOUTUBE_API_KEY:
st.warning("π¨ **YouTube API Key not found!** Please add `YOUTUBE_API_KEY` to your Hugging Face Space secrets or environment variables.")
st.info("Instructions on how to add secrets to Hugging Face Spaces can be found in their documentation.")
st.stop() # Stop execution if API key is missing
# Load the sentiment model with a spinner
with st.spinner("π Loading sentiment model... This might take a moment if it's the first run (downloading model)."):
sentiment_classifier = load_sentiment_model()
# User input for YouTube URL
youtube_url = st.text_input(
"π Enter YouTube Video URL:",
"https://www.youtube.com/watch?v=dQw4w9WgXcQ", # Example URL (Rick Astley - Never Gonna Give You Up)
help="e.g., https://www.youtube.com/watch?v=dQw4w9WgXcQ"
)
# Slider for maximum comments to fetch
max_comments = st.slider(
"π Max Comments to Fetch (approx.):",
min_value=50, max_value=2000, value=500, step=50,
help="Fetching more comments takes longer and uses more API quota."
)
# Button to trigger analysis
if st.button("β¨ Analyze Comments", type="primary"):
if not youtube_url:
st.error("β Please enter a YouTube video URL to analyze.")
else:
video_id = get_video_id(youtube_url)
if not video_id:
st.error("β Invalid YouTube URL. Please make sure it's a valid video URL (e.g., starts with youtube.com/watch?v= or youtu.be/).")
else:
st.info(f"Fetching comments for video ID: `{video_id}`")
# Fetch comments with a spinner
with st.spinner(f"π₯ Fetching up to {max_comments} comments from YouTube..."):
comments = fetch_comments(video_id, YOUTUBE_API_KEY, max_comments)
if not comments:
st.warning("β οΈ No comments found for this video, or an error occurred while fetching. Ensure comments are enabled and public.")
else:
st.success(f"β
Successfully fetched {len(comments)} comments.")
# Analyze sentiments with a spinner
with st.spinner("π§ Analyzing sentiment of comments..."):
sentiment_df = analyze_sentiments(comments, sentiment_classifier)
if sentiment_df.empty:
st.info("No comments were successfully analyzed for sentiment.")
else:
# Display results
st.subheader("π Sentiment Analysis Results:")
# Display summary statistics
total_analyzed = len(sentiment_df)
positive_count = sentiment_df[sentiment_df['Sentiment'] == 'POSITIVE'].shape[0]
negative_count = sentiment_df[sentiment_df['Sentiment'] == 'NEGATIVE'].shape[0]
st.write(f"**Total comments analyzed:** `{total_analyzed}`")
st.write(f"**Positive sentiments:** `{positive_count}` ({(positive_count/total_analyzed*100):.1f}%)")
st.write(f"**Negative sentiments:** `{negative_count}` ({(negative_count/total_analyzed*100):.1f}%)")
# Plot sentiment distribution
plot_sentiment_distribution(sentiment_df)
# Display individual sentiment scores
display_sentiment_scores(sentiment_df)
st.markdown("---")
st.markdown("This is a Build Up project developed last night with β€οΈ as an example of a modular addition to support the reporting of TFGBV platforms in Kenya .")
|