FocusFlow Assistant commited on
Commit ·
2847ff4
1
Parent(s): 068aa2f
Gracefully disable YouTube in Cloud Mode
Browse files- app.py +68 -40
- backend/config.py +5 -0
- backend/main.py +9 -0
- backend/rag_engine.py +7 -0
- push_all.sh +23 -0
app.py
CHANGED
|
@@ -411,6 +411,16 @@ if "expanded_topics" not in st.session_state: st.session_state.expanded_topics =
|
|
| 411 |
if "show_analytics" not in st.session_state: st.session_state.show_analytics = False
|
| 412 |
if "topic_scores" not in st.session_state: st.session_state.topic_scores = {} # Track quiz performance by topic_id
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
# Helper function to add auth headers to all API requests
|
| 415 |
def get_headers():
|
| 416 |
"""Get auth headers for API requests — uses Firebase token if available."""
|
|
@@ -1138,57 +1148,75 @@ if not st.session_state.focus_mode:
|
|
| 1138 |
|
| 1139 |
|
| 1140 |
# URL Input
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
if
|
| 1154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
else:
|
| 1156 |
-
|
| 1157 |
-
with st.spinner("
|
| 1158 |
try:
|
| 1159 |
-
resp = requests.post(f"{API_URL}/
|
| 1160 |
if resp.status_code == 200:
|
| 1161 |
-
st.success("✅
|
| 1162 |
time.sleep(1)
|
| 1163 |
st.rerun()
|
| 1164 |
else:
|
| 1165 |
error_detail = resp.json().get('detail', resp.text)
|
| 1166 |
-
|
| 1167 |
-
st.error("❌ No captions found. Try a video with CC enabled.")
|
| 1168 |
-
elif "Could not reach any transcript" in str(error_detail):
|
| 1169 |
-
st.error("⚠️ Transcript service unavailable. Try again later.")
|
| 1170 |
-
else:
|
| 1171 |
-
st.error(f"Failed: {error_detail}")
|
| 1172 |
except requests.Timeout:
|
| 1173 |
st.error("⏱️ Request timed out. Please try again.")
|
| 1174 |
except Exception as e:
|
| 1175 |
st.error(f"Error: {str(e)}")
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
st.error(f"Error: {str(e)}")
|
| 1192 |
|
| 1193 |
# --- FOCUS MODE UI ---
|
| 1194 |
if st.session_state.focus_mode:
|
|
|
|
| 411 |
if "show_analytics" not in st.session_state: st.session_state.show_analytics = False
|
| 412 |
if "topic_scores" not in st.session_state: st.session_state.topic_scores = {} # Track quiz performance by topic_id
|
| 413 |
|
| 414 |
+
if "app_config" not in st.session_state:
|
| 415 |
+
try:
|
| 416 |
+
resp = requests.get(f"{API_URL}/config", timeout=5)
|
| 417 |
+
if resp.status_code == 200:
|
| 418 |
+
st.session_state.app_config = resp.json()
|
| 419 |
+
else:
|
| 420 |
+
st.session_state.app_config = {"youtube_enabled": True}
|
| 421 |
+
except Exception:
|
| 422 |
+
st.session_state.app_config = {"youtube_enabled": True}
|
| 423 |
+
|
| 424 |
# Helper function to add auth headers to all API requests
|
| 425 |
def get_headers():
|
| 426 |
"""Get auth headers for API requests — uses Firebase token if available."""
|
|
|
|
| 1148 |
|
| 1149 |
|
| 1150 |
# URL Input
|
| 1151 |
+
youtube_enabled = st.session_state.get("app_config", {}).get("youtube_enabled", True)
|
| 1152 |
+
|
| 1153 |
+
if youtube_enabled:
|
| 1154 |
+
with st.expander("+ Add URL / YouTube"):
|
| 1155 |
+
url_input = st.text_input("URL", placeholder="https://youtube.com/... or any webpage", label_visibility="collapsed")
|
| 1156 |
+
if st.button("Process URL", use_container_width=True):
|
| 1157 |
+
if not url_input:
|
| 1158 |
+
st.warning("Please enter a URL")
|
| 1159 |
+
else:
|
| 1160 |
+
import re as re_mod
|
| 1161 |
+
is_youtube = "youtube.com" in url_input or "youtu.be" in url_input
|
| 1162 |
+
|
| 1163 |
+
if is_youtube:
|
| 1164 |
+
# Extract video ID
|
| 1165 |
+
vid_match = re_mod.search(r'(?:v=|youtu\.be/|shorts/|embed/)([a-zA-Z0-9_-]{11})', url_input)
|
| 1166 |
+
if not vid_match:
|
| 1167 |
+
st.error("❌ Invalid YouTube URL format. Supported: youtube.com/watch?v=ID, youtu.be/ID, or youtube.com/shorts/ID")
|
| 1168 |
+
else:
|
| 1169 |
+
video_id = vid_match.group(1)
|
| 1170 |
+
with st.spinner("⏳ Fetching transcript via Invidious..."):
|
| 1171 |
+
try:
|
| 1172 |
+
resp = requests.post(f"{API_URL}/ingest_youtube", json={"video_id": video_id}, headers=get_headers(), timeout=120)
|
| 1173 |
+
if resp.status_code == 200:
|
| 1174 |
+
st.success("✅ YouTube transcript processed successfully!")
|
| 1175 |
+
time.sleep(1)
|
| 1176 |
+
st.rerun()
|
| 1177 |
+
else:
|
| 1178 |
+
error_detail = resp.json().get('detail', resp.text)
|
| 1179 |
+
if "No captions available" in str(error_detail):
|
| 1180 |
+
st.error("❌ No captions found. Try a video with CC enabled.")
|
| 1181 |
+
elif "Could not reach any transcript" in str(error_detail):
|
| 1182 |
+
st.error("⚠️ Transcript service unavailable. Try again later.")
|
| 1183 |
+
else:
|
| 1184 |
+
st.error(f"Failed: {error_detail}")
|
| 1185 |
+
except requests.Timeout:
|
| 1186 |
+
st.error("⏱️ Request timed out. Please try again.")
|
| 1187 |
+
except Exception as e:
|
| 1188 |
+
st.error(f"Error: {str(e)}")
|
| 1189 |
else:
|
| 1190 |
+
# Non-YouTube URL: use server-side ingestion
|
| 1191 |
+
with st.spinner("Fetching content..."):
|
| 1192 |
try:
|
| 1193 |
+
resp = requests.post(f"{API_URL}/ingest_url", json={"url": url_input}, headers=get_headers(), timeout=120)
|
| 1194 |
if resp.status_code == 200:
|
| 1195 |
+
st.success(f"✅ {resp.json().get('message', 'Content added!')}")
|
| 1196 |
time.sleep(1)
|
| 1197 |
st.rerun()
|
| 1198 |
else:
|
| 1199 |
error_detail = resp.json().get('detail', resp.text)
|
| 1200 |
+
st.error(f"Failed: {error_detail}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
except requests.Timeout:
|
| 1202 |
st.error("⏱️ Request timed out. Please try again.")
|
| 1203 |
except Exception as e:
|
| 1204 |
st.error(f"Error: {str(e)}")
|
| 1205 |
+
else:
|
| 1206 |
+
with st.expander("▶️ Add YouTube Video"):
|
| 1207 |
+
st.info(
|
| 1208 |
+
"⚠️ **YouTube is only available in local mode.**\n\n"
|
| 1209 |
+
"HuggingFace Spaces blocks outbound network requests "
|
| 1210 |
+
"which prevents YouTube transcript fetching.\n\n"
|
| 1211 |
+
"**Alternatives:**\n"
|
| 1212 |
+
"- 📄 Upload a PDF of your notes instead\n"
|
| 1213 |
+
"- 💻 Run FocusFlow locally to use YouTube\n"
|
| 1214 |
+
"- 📋 Paste text directly (coming soon)"
|
| 1215 |
+
)
|
| 1216 |
+
st.markdown(
|
| 1217 |
+
"[▶️ How to run locally](https://github.com/thesivarohith/focusflow#local-setup)",
|
| 1218 |
+
unsafe_allow_html=False
|
| 1219 |
+
)
|
|
|
|
| 1220 |
|
| 1221 |
# --- FOCUS MODE UI ---
|
| 1222 |
if st.session_state.focus_mode:
|
backend/config.py
CHANGED
|
@@ -13,6 +13,11 @@ class LLMProvider(Enum):
|
|
| 13 |
# Read from environment variable, default to Ollama (local)
|
| 14 |
USE_PROVIDER = os.getenv("LLM_PROVIDER", "ollama").lower()
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# Configuration for both providers
|
| 17 |
CONFIG = {
|
| 18 |
"llm_provider": LLMProvider.OLLAMA if USE_PROVIDER == "ollama" else LLMProvider.HUGGINGFACE,
|
|
|
|
| 13 |
# Read from environment variable, default to Ollama (local)
|
| 14 |
USE_PROVIDER = os.getenv("LLM_PROVIDER", "ollama").lower()
|
| 15 |
|
| 16 |
+
# Set DEPLOYMENT_MODE=cloud in HuggingFace Spaces secrets
|
| 17 |
+
# Leave unset or set to "local" for local development
|
| 18 |
+
DEPLOYMENT_MODE = os.environ.get("DEPLOYMENT_MODE", "local").lower()
|
| 19 |
+
IS_CLOUD = DEPLOYMENT_MODE == "cloud"
|
| 20 |
+
|
| 21 |
# Configuration for both providers
|
| 22 |
CONFIG = {
|
| 23 |
"llm_provider": LLMProvider.OLLAMA if USE_PROVIDER == "ollama" else LLMProvider.HUGGINGFACE,
|
backend/main.py
CHANGED
|
@@ -20,6 +20,15 @@ def health_check():
|
|
| 20 |
"""Health check endpoint"""
|
| 21 |
return {"status": "healthy"}
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# Dependency
|
| 24 |
def get_db():
|
| 25 |
db = SessionLocal()
|
|
|
|
| 20 |
"""Health check endpoint"""
|
| 21 |
return {"status": "healthy"}
|
| 22 |
|
| 23 |
+
@app.get("/config")
|
| 24 |
+
async def get_config():
|
| 25 |
+
from backend.config import IS_CLOUD, DEPLOYMENT_MODE
|
| 26 |
+
return {
|
| 27 |
+
"is_cloud": IS_CLOUD,
|
| 28 |
+
"deployment_mode": DEPLOYMENT_MODE,
|
| 29 |
+
"youtube_enabled": not IS_CLOUD
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
# Dependency
|
| 33 |
def get_db():
|
| 34 |
db = SessionLocal()
|
backend/rag_engine.py
CHANGED
|
@@ -26,6 +26,13 @@ INVIDIOUS_INSTANCES = [
|
|
| 26 |
|
| 27 |
def get_youtube_transcript(video_id: str) -> str:
|
| 28 |
"""Fetch YouTube transcripts via the Invidious API to bypass network blocks."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# Step 1: Try each Invidious instance until one works
|
| 30 |
captions_data = None
|
| 31 |
last_error = None
|
|
|
|
| 26 |
|
| 27 |
def get_youtube_transcript(video_id: str) -> str:
|
| 28 |
"""Fetch YouTube transcripts via the Invidious API to bypass network blocks."""
|
| 29 |
+
from backend.config import IS_CLOUD
|
| 30 |
+
if IS_CLOUD:
|
| 31 |
+
raise ValueError(
|
| 32 |
+
"YouTube is not available in cloud mode. "
|
| 33 |
+
"Please upload a PDF instead."
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
# Step 1: Try each Invidious instance until one works
|
| 37 |
captions_data = None
|
| 38 |
last_error = None
|
push_all.sh
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# A script to easily commit and push to both GitHub and HuggingFace
|
| 3 |
+
|
| 4 |
+
if [ -z "$1" ]; then
|
| 5 |
+
echo "Usage: ./push_all.sh \"Commit message\""
|
| 6 |
+
exit 1
|
| 7 |
+
fi
|
| 8 |
+
|
| 9 |
+
COMMIT_MSG=$1
|
| 10 |
+
|
| 11 |
+
echo "Staging changes..."
|
| 12 |
+
git add .
|
| 13 |
+
|
| 14 |
+
echo "Committing with message: $COMMIT_MSG"
|
| 15 |
+
git commit -m "$COMMIT_MSG"
|
| 16 |
+
|
| 17 |
+
echo "Pushing to HuggingFace (origin)..."
|
| 18 |
+
git push origin main
|
| 19 |
+
|
| 20 |
+
echo "Pushing to GitHub (github)..."
|
| 21 |
+
git push github main
|
| 22 |
+
|
| 23 |
+
echo "All done!"
|