import os import re import requests import markdown from dotenv import load_dotenv from langchain_google_genai import ChatGoogleGenerativeAI load_dotenv() # --------------------------------------------------------------- # Gemini LLM # --------------------------------------------------------------- llm = ChatGoogleGenerativeAI( model="gemini-2.5-flash", google_api_key=os.getenv("GOOGLE_API_KEY"), temperature=0.3, ) # --------------------------------------------------------------- # Extract video ID from any YouTube URL format # --------------------------------------------------------------- def extract_video_id(url: str) -> str: patterns = [ r"(?:v=)([A-Za-z0-9_-]{11})", r"youtu\.be/([A-Za-z0-9_-]{11})", r"shorts/([A-Za-z0-9_-]{11})", r"embed/([A-Za-z0-9_-]{11})", ] for pattern in patterns: match = re.search(pattern, url) if match: return match.group(1) raise ValueError( f"Could not extract a valid YouTube video ID from: {url}\n" "Please use a standard YouTube link like https://www.youtube.com/watch?v=..." ) # --------------------------------------------------------------- # Fetch transcript via Supadata API # Sign up free at https://supadata.ai — 500 transcripts/month # Add SUPADATA_API_KEY to HF Space secrets # --------------------------------------------------------------- def fetch_transcript(video_id: str) -> str: api_key = os.getenv("SUPADATA_API_KEY") if not api_key: raise EnvironmentError( "SUPADATA_API_KEY is not set.\n" "• Sign up free at https://supadata.ai\n" "• Add SUPADATA_API_KEY to HF Space → Settings → Variables and Secrets" ) try: resp = requests.get( "https://api.supadata.ai/v1/youtube/transcript", headers={"x-api-key": api_key}, params={"videoId": video_id, "lang": "en"}, timeout=30, ) except requests.exceptions.ConnectionError: raise ConnectionError("Could not reach Supadata API. Check your internet connection.") except requests.exceptions.Timeout: raise TimeoutError("Supadata API timed out. Try again.") if resp.status_code == 401: raise EnvironmentError( "Invalid SUPADATA_API_KEY. Check the key in your HF Space secrets." ) if resp.status_code == 404: raise ValueError( "No transcript found for this video.\n" "The video may have no subtitles, be private, or age-restricted." ) if resp.status_code == 429: raise RuntimeError( "Supadata free tier limit reached (500/month).\n" "Upgrade at https://supadata.ai or try again next month." ) if not resp.ok: raise RuntimeError(f"Supadata API error {resp.status_code}: {resp.text}") data = resp.json() # Supadata returns: { "content": [ {"text": "...", "offset": 0, "duration": 2} ] } content = data.get("content", []) if not content: raise ValueError("Supadata returned an empty transcript.") # Join all text chunks into a single string transcript = " ".join( chunk.get("text", "") for chunk in content if chunk.get("text") ).strip() if not transcript: raise ValueError("Transcript was empty after processing.") return transcript # --------------------------------------------------------------- # Main transcript entry point # --------------------------------------------------------------- def extract_transcript(url: str) -> str: video_id = extract_video_id(url) return fetch_transcript(video_id) # --------------------------------------------------------------- # Summarize transcript into a Markdown article # --------------------------------------------------------------- def summarize(text: str) -> str: safe_text = text[:8000] prompt = f"""Convert this YouTube transcript into a HIGH-QUALITY Markdown blog article. STRICT RULES: - Use ## for main sections - Use ### for sub-sections - Use bullet points (-) for lists - Each bullet on its own line - Clean, readable prose — no filler words - No inline messy formatting - Do NOT include a title (it will be added separately) Transcript: {safe_text} """ return llm.invoke(prompt).content # --------------------------------------------------------------- # Generate a short blog title # --------------------------------------------------------------- def generate_title(article: str) -> str: prompt = f"""Generate ONLY ONE short, catchy blog post title for this article. Rules: - One line only - No quotes, no explanation, no numbering - Suitable as an HTML tag Article (first 2000 chars): {article[:2000]} """ raw = llm.invoke(prompt).content.strip() return raw.split("\n")[0].strip().strip('"').strip("'") # --------------------------------------------------------------- # Full pipeline: URL → article text # --------------------------------------------------------------- def generate_article_from_url(url: str) -> str: transcript = extract_transcript(url) return summarize(transcript) # --------------------------------------------------------------- # Convert Markdown article → styled HTML file # --------------------------------------------------------------- def generate_web_files(article: str) -> None: try: title = generate_title(article) except Exception: title = "YouTube Generated Article" word_count = len(article.split()) reading_time = max(1, word_count // 200) html_body = markdown.markdown(article, extensions=["extra"]) html_content = f"""<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{title}

{title}

⏱ {reading_time} min read  |  Generated from YouTube

{html_body} """ with open("generated_article.html", "w", encoding="utf-8") as f: f.write(html_content)