Spaces:
Sleeping
Sleeping
| 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 <title> 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> | |
| <style> | |
| body {{ | |
| font-family: Georgia, 'Times New Roman', serif; | |
| max-width: 750px; | |
| margin: 0 auto; | |
| padding: 24px 20px; | |
| line-height: 1.8; | |
| color: #1a1a1a; | |
| background: #fff; | |
| }} | |
| h1 {{ font-size: 2rem; margin-bottom: 4px; line-height: 1.3; }} | |
| h2 {{ font-size: 1.4rem; margin-top: 2rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 4px; }} | |
| h3 {{ font-size: 1.15rem; margin-top: 1.5rem; }} | |
| p {{ margin: 0.9rem 0; }} | |
| ul, ol {{ padding-left: 1.4rem; margin: 0.7rem 0; }} | |
| li {{ margin: 0.3rem 0; }} | |
| .meta {{ color: #888; font-size: 0.9rem; font-family: sans-serif; margin-bottom: 2rem; }} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>{title}</h1> | |
| <p class="meta">⏱ {reading_time} min read | Generated from YouTube</p> | |
| {html_body} | |
| </body> | |
| </html> | |
| """ | |
| with open("generated_article.html", "w", encoding="utf-8") as f: | |
| f.write(html_content) |