youtube-article-generator / content_engine.py
kishore-9's picture
Update content_engine.py
1a3bea4 verified
Raw
History Blame Contribute Delete
6.89 kB
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 &nbsp;|&nbsp; Generated from YouTube</p>
{html_body}
</body>
</html>
"""
with open("generated_article.html", "w", encoding="utf-8") as f:
f.write(html_content)