|
""" |
|
Physics Chapter Video Generator |
|
Creates educational videos by combining title cards with relevant content. |
|
""" |
|
|
|
import re, shutil, subprocess, textwrap, os, tempfile |
|
from pathlib import Path |
|
from typing import List, Optional |
|
import gradio as gr |
|
import random |
|
|
|
print("CWD:", os.getcwd()) |
|
print("cookies.txt exists:", os.path.exists("./cookies/cookies.txt")) |
|
|
|
|
|
|
|
PROXY_USERNAME = "ujwal_CmiMZ" |
|
PROXY_PASSWORD = "xJv4DChht5P6y+u" |
|
PROXY_COUNTRY = "US" |
|
|
|
|
|
PROXY_PORTS = [8001, 8002, 8003, 8004, 8005] |
|
PROXY_HOST = "dc.oxylabs.io" |
|
|
|
def get_random_proxy(): |
|
port = random.choice(PROXY_PORTS) |
|
return f"http://user-{PROXY_USERNAME}-country-{PROXY_COUNTRY}:{PROXY_PASSWORD}@{PROXY_HOST}:{port}" |
|
|
|
|
|
|
|
TITLE_DUR = 3 |
|
SIZE = "1280x720" |
|
FPS = 30 |
|
CRF = 28 |
|
PRESET = "ultrafast" |
|
YT_MAX_RESULTS = 2 |
|
MAX_VIDEO_LENGTH = 30 |
|
MAX_TOPICS = 8 |
|
|
|
|
|
|
|
def run_cmd(cmd: list[str], timeout: int = 120) -> bool: |
|
"""Run command with timeout and proper error handling""" |
|
try: |
|
result = subprocess.run( |
|
cmd, |
|
check=True, |
|
timeout=timeout, |
|
capture_output=True, |
|
text=True |
|
) |
|
return True |
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: |
|
print(f"Command failed:\n{' '.join(cmd)}\nError:\n{e.stderr if hasattr(e, 'stderr') else str(e)}") |
|
return False |
|
|
|
def yt_urls(query: str, max_results: int) -> List[str]: |
|
"""Get YouTube URLs for search query via proxy""" |
|
try: |
|
import requests |
|
from youtube_search import YoutubeSearch |
|
|
|
proxy_url = get_random_proxy() |
|
proxies = { |
|
"http": proxy_url, |
|
"https": proxy_url, |
|
} |
|
|
|
|
|
original_get = requests.get |
|
|
|
def proxied_get(*args, **kwargs): |
|
kwargs["proxies"] = proxies |
|
kwargs["verify"] = False |
|
return original_get(*args, **kwargs) |
|
|
|
requests.get = proxied_get |
|
results = YoutubeSearch(query, max_results=max_results).to_dict() |
|
requests.get = original_get |
|
|
|
return ["https://www.youtube.com" + r["url_suffix"] for r in results] |
|
except Exception as e: |
|
print(f"YouTube search failed: {e}") |
|
return [] |
|
|
|
|
|
def safe_filename(name: str) -> str: |
|
"""Create safe filename""" |
|
return re.sub(r"[^\w\-\.]", "_", name)[:50] |
|
|
|
|
|
def dl_video(url: str, out: Path) -> bool: |
|
"""Download video with length limit using rotating proxy""" |
|
out.parent.mkdir(exist_ok=True) |
|
proxy = get_random_proxy() |
|
|
|
cmd = [ |
|
"yt-dlp", |
|
"--match-filter", f"duration<{MAX_VIDEO_LENGTH}", |
|
"-f", "mp4", |
|
"--merge-output-format", "mp4", |
|
"-o", str(out), |
|
"--no-playlist", |
|
|
|
"--proxy", proxy, |
|
"--cookies", "./cookies/cookies.txt", |
|
url, |
|
] |
|
return run_cmd(cmd, timeout=60) |
|
|
|
|
|
def make_card(text: str, out: Path, dur: int = TITLE_DUR) -> bool: |
|
"""Create title card with text""" |
|
|
|
wrapped = textwrap.wrap(text, width=25) |
|
safe_text = "\\n".join(w.replace("'", r"\\'") for w in wrapped) |
|
|
|
|
|
cmd = [ |
|
"ffmpeg", |
|
"-loglevel", "error", |
|
"-f", "lavfi", "-i", f"color=c=navy:s={SIZE}:d={dur}", |
|
"-f", "lavfi", "-i", "anullsrc=r=44100:cl=stereo", |
|
"-vf", ( |
|
f"drawtext=text='{safe_text}':fontcolor=white:fontsize=60:" |
|
"x=(w-text_w)/2:y=(h-text_h)/2:fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" |
|
), |
|
"-shortest", "-r", str(FPS), |
|
"-c:v", "libx264", "-preset", PRESET, "-crf", str(CRF), |
|
"-c:a", "aac", "-b:a", "96k", |
|
"-movflags", "+faststart", |
|
"-y", str(out), |
|
] |
|
return run_cmd(cmd) |
|
|
|
def extract_topics(text: str) -> List[str]: |
|
"""Extract topics from input text""" |
|
topics = [] |
|
for line in text.splitlines(): |
|
line = line.strip() |
|
if not line or len(topics) >= MAX_TOPICS: |
|
continue |
|
|
|
|
|
if re.match(r"^\d+[\.)]\s+.+", line): |
|
topic = re.sub(r"^\d+[\.)]\s*", "", line) |
|
topics.append(topic) |
|
|
|
elif re.match(r"^#+\s+.+", line): |
|
topic = re.sub(r"^#+\s*", "", line) |
|
topics.append(topic) |
|
|
|
elif line.isupper() and 3 <= len(line) <= 50: |
|
topics.append(line.title()) |
|
|
|
elif len(line) > 3 and not line.startswith(('http', 'www')): |
|
topics.append(line) |
|
|
|
return topics[:MAX_TOPICS] |
|
|
|
def create_physics_video(chapter_text: str, progress=gr.Progress()) -> Optional[str]: |
|
"""Generate educational physics video from chapter topics""" |
|
if not chapter_text.strip(): |
|
return None |
|
|
|
progress(0, desc="Extracting topics...") |
|
topics = extract_topics(chapter_text) |
|
|
|
if not topics: |
|
return None |
|
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir: |
|
temp_path = Path(temp_dir) |
|
concat_paths: List[Path] = [] |
|
|
|
total_steps = len(topics) * 2 + 3 |
|
current_step = 0 |
|
|
|
|
|
progress(current_step/total_steps, desc="Creating opening card...") |
|
opening = temp_path / "00_opening.mp4" |
|
if make_card("Physics Chapter Overview", opening): |
|
concat_paths.append(opening) |
|
current_step += 1 |
|
|
|
|
|
for idx, topic in enumerate(topics, 1): |
|
|
|
progress(current_step/total_steps, desc=f"Creating card for: {topic[:30]}...") |
|
card = temp_path / f"title_{idx:02d}.mp4" |
|
if make_card(topic, card): |
|
concat_paths.append(card) |
|
current_step += 1 |
|
|
|
|
|
progress(current_step/total_steps, desc=f"Searching video for: {topic[:30]}...") |
|
video_found = False |
|
|
|
for url in yt_urls(f"{topic} physics explanation", YT_MAX_RESULTS): |
|
vid_id_match = re.search(r"(?:v=|be/|shorts/)([\w\-]{11})", url) |
|
if not vid_id_match: |
|
continue |
|
|
|
vid_path = temp_path / f"{safe_filename(vid_id_match.group(1))}.mp4" |
|
if dl_video(url, vid_path): |
|
concat_paths.append(vid_path) |
|
video_found = True |
|
break |
|
|
|
if not video_found: |
|
|
|
placeholder = temp_path / f"placeholder_{idx:02d}.mp4" |
|
if make_card(f"Exploring: {topic}", placeholder, dur=5): |
|
concat_paths.append(placeholder) |
|
|
|
current_step += 1 |
|
|
|
|
|
progress(current_step/total_steps, desc="Creating closing card...") |
|
closing = temp_path / "zz_closing.mp4" |
|
if make_card("Thank you for learning!", closing): |
|
concat_paths.append(closing) |
|
current_step += 1 |
|
|
|
if len(concat_paths) < 2: |
|
return None |
|
|
|
|
|
list_file = temp_path / "list.txt" |
|
list_file.write_text( |
|
"".join(f"file '{p.absolute()}'\n" for p in concat_paths), |
|
encoding="utf-8" |
|
) |
|
|
|
|
|
output_path = "physics_chapter_video.mp4" |
|
|
|
|
|
progress(current_step/total_steps, desc="Creating final video...") |
|
cmd = [ |
|
"ffmpeg", |
|
"-loglevel", "error", |
|
"-f", "concat", "-safe", "0", "-i", str(list_file), |
|
"-c:v", "libx264", "-preset", PRESET, "-crf", str(CRF), |
|
"-c:a", "aac", "-b:a", "128k", |
|
"-movflags", "+faststart", |
|
"-y", output_path, |
|
] |
|
|
|
if run_cmd(cmd, timeout=300): |
|
return output_path |
|
|
|
return None |
|
|
|
|
|
def create_interface(): |
|
"""Setup the web interface""" |
|
with gr.Blocks(title="Physics Video Generator", theme=gr.themes.Soft()) as app: |
|
gr.Markdown(""" |
|
# Physics Video Generator |
|
|
|
Transform your physics topics into engaging educational videos! This tool will: |
|
- Create professional title slides for each topic |
|
- Find relevant educational content |
|
- Combine everything into a complete video |
|
|
|
**How to use:** Enter your topics one per line, or use numbered lists, or markdown headers. |
|
""") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
chapter_input = gr.Textbox( |
|
label="Chapter Topics", |
|
placeholder="""Enter topics like: |
|
1. Newton's Laws of Motion |
|
2. Force and Acceleration |
|
3. Momentum and Impulse |
|
4. Energy Conservation |
|
5. Circular Motion |
|
|
|
Or: |
|
# Kinematics |
|
# Dynamics |
|
# Thermodynamics""", |
|
lines=10, |
|
max_lines=15 |
|
) |
|
|
|
generate_btn = gr.Button("Create Physics Video", variant="primary", size="lg") |
|
|
|
with gr.Column(): |
|
video_output = gr.Video(label="Your Physics Video") |
|
|
|
gr.Markdown(""" |
|
### Important Notes: |
|
- Processing typically takes 2-5 minutes |
|
- Videos are optimized for educational use |
|
- Limited to 8 topics per session |
|
- Each video segment is capped at 30 seconds |
|
""") |
|
|
|
generate_btn.click( |
|
fn=create_physics_video, |
|
inputs=[chapter_input], |
|
outputs=[video_output], |
|
show_progress=True |
|
) |
|
|
|
|
|
gr.Examples( |
|
examples=[ |
|
["1. Newton's First Law\n2. Newton's Second Law\n3. Newton's Third Law\n4. Applications of Newton's Laws"], |
|
["# Wave Motion\n# Sound Waves\n# Light Waves\n# Electromagnetic Spectrum"], |
|
["THERMODYNAMICS\nHEAT TRANSFER\nENTROPY\nCARNOT CYCLE"], |
|
["Quantum Mechanics Basics\nWave-Particle Duality\nHeisenberg Uncertainty Principle\nQuantum Tunneling"] |
|
], |
|
inputs=[chapter_input], |
|
label="Example Topics" |
|
) |
|
|
|
return app |
|
|
|
if __name__ == "__main__": |
|
app = create_interface() |
|
app.queue(max_size=3) |
|
app.launch( |
|
share=False, |
|
server_name="0.0.0.0", |
|
server_port=7860 |
|
) |