yt / app.py
ayush77uh's picture
vv
6d8fd1d
Raw
History Blame Contribute Delete
13.2 kB
import os
import time
import random
import glob
import subprocess
import threading
import shutil
import tempfile
import gradio as gr
from dotenv import load_dotenv
# Load env vars from .env file (if running locally)
load_dotenv()
# Import Local Modules
from src.generator import StoryGenerator
from src.gemini_generator import GeminiScriptGenerator
from src.asset_manager import AssetManager
from src.drive_uploader import upload_to_drive
from src.music_video_generator import process_music_videos
# NOTE: src.pro_audio_generator is imported dynamically
# --- Global State for UI ---
CURRENT_STATUS = "Initializing..."
VIDEO_COUNT = 0
LAST_VIDEO = "None"
LAST_VIDEO_PATH = None
def update_status(msg, video_path=None):
global CURRENT_STATUS, LAST_VIDEO, LAST_VIDEO_PATH
CURRENT_STATUS = msg
if video_path:
LAST_VIDEO = os.path.basename(video_path)
LAST_VIDEO_PATH = video_path
print(f"ℹ️ STATUS: {msg}", flush=True)
def extract_spoken_script(script_text):
if "SCRIPT:" not in script_text:
return script_text
spoken = script_text.split("SCRIPT:", 1)[1]
metadata_labels = ["TITLE SUGGESTIONS:", "THUMBNAIL TEXT IDEAS:"]
for label in metadata_labels:
if label in spoken:
spoken = spoken.split(label, 1)[0]
return spoken.strip()
# --- Configuration ---
ENABLE_MUSIC_VIDEO_MODE = os.getenv("ENABLE_MUSIC_VIDEO_MODE", "False").lower() == "true"
MUSIC_VIDEO_SONGS_FOLDER_ID = os.getenv("DRIVE_SONGS_FOLDER")
MUSIC_VIDEO_VISUALS_FOLDER_ID = os.getenv("DRIVE_VISUALS_FOLDER")
# --- Asset Manager (Handles Drive + Local Backgrounds) ---
asset_mgr = AssetManager()
# --- FFmpeg Video Renderer ---
def _has_audio(ffmpeg_path, file_path):
"""Check if a video file has an audio stream."""
try:
ffprobe = shutil.which("ffprobe") or ffmpeg_path.replace("ffmpeg", "ffprobe")
result = subprocess.run(
[ffprobe, "-v", "quiet", "-select_streams", "a",
"-show_entries", "stream=codec_type", "-of", "csv=p=0", file_path],
capture_output=True, text=True, timeout=10)
return "audio" in result.stdout
except:
return False
def create_video(audio_path, bg_video, topic):
"""
Renders final video:
- Multiple background clips rotate behind voiceover
- Scales/crops to 1920x1080 landscape
- Uses voiceover audio only
"""
ffmpeg_path = shutil.which("ffmpeg") or "ffmpeg"
output_path = f"output_{topic.replace(' ', '_')}_{int(time.time())}.mp4"
try:
bg_videos = bg_video if isinstance(bg_video, list) else [bg_video]
bg_videos = [clip for clip in bg_videos if clip and os.path.exists(clip)]
if not bg_videos:
print("❌ No valid background clips for render.", flush=True)
return None
concat_file = os.path.join(tempfile.gettempdir(), f"broll_concat_{int(time.time())}.txt")
with open(concat_file, "w", encoding="utf-8") as f:
for clip in bg_videos:
safe_clip = os.path.abspath(clip).replace("\\", "/").replace("'", "'\\''")
f.write(f"file '{safe_clip}'\n")
print(f"🎬 Rendering with {len(bg_videos)} rotating B-roll clips...", flush=True)
cmd = [
ffmpeg_path,
"-stream_loop", "-1",
"-f", "concat",
"-safe", "0",
"-i", concat_file,
"-i", audio_path,
"-filter_complex",
"[0:v]scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080[outv]",
"-map", "[outv]", "-map", "1:a",
"-c:v", "libx264", "-preset", "ultrafast", "-crf", "23",
"-c:a", "aac", "-b:a", "192k",
"-shortest", "-y", output_path
]
# Get audio duration for progress tracking
audio_dur = None
try:
ffprobe = shutil.which("ffprobe") or "ffprobe"
dur_result = subprocess.run(
[ffprobe, "-v", "quiet", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", audio_path],
capture_output=True, text=True, timeout=10)
if dur_result.returncode == 0:
audio_dur = float(dur_result.stdout.strip())
print(f"🎬 Rendering {audio_dur:.0f}s of video...", flush=True)
except: pass
if not audio_dur:
print(f"🎬 Rendering Video: {output_path}...", flush=True)
render_start = time.time()
# Use Popen for live progress
import re as _re
process = subprocess.Popen(
cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
universal_newlines=True
)
last_progress = 0
while True:
line = process.stderr.readline()
if not line and process.poll() is not None:
break
# Parse FFmpeg progress: "time=00:01:23.45"
time_match = _re.search(r'time=(\d+):(\d+):(\d+\.\d+)', line)
if time_match and audio_dur:
h, m, s = time_match.groups()
current = int(h) * 3600 + int(m) * 60 + float(s)
pct = min(int((current / audio_dur) * 100), 99)
# Log every 10%
if pct >= last_progress + 10:
last_progress = pct
elapsed = time.time() - render_start
fps_match = _re.search(r'fps=\s*(\d+)', line)
fps = fps_match.group(1) if fps_match else "?"
eta = (elapsed / max(pct, 1)) * (100 - pct)
print(f" 🎬 Render: {pct}% ({current:.0f}/{audio_dur:.0f}s) | {fps} fps | ETA: {eta:.0f}s", flush=True)
returncode = process.wait()
if returncode != 0:
print(f"❌ FFmpeg Error!", flush=True)
return None
render_time = time.time() - render_start
size_mb = os.path.getsize(output_path) / (1024*1024)
# Get final video duration
dur = f"{audio_dur:.1f}s" if audio_dur else "?"
print(f"✅ Video Rendered: {output_path}", flush=True)
print(f" 📊 Duration: {dur} | Size: {size_mb:.1f} MB | Render time: {render_time:.0f}s", flush=True)
if os.path.exists(concat_file):
os.remove(concat_file)
return output_path
except Exception as e:
print(f"❌ Video Render Failed: {e}", flush=True)
return None
# --- Main Generator Loop ---
def main_loop():
global VIDEO_COUNT, LAST_VIDEO, LAST_VIDEO_PATH
update_status("Starting Generator Loop...")
# 0. Wait for UI to come up
time.sleep(5)
# 1. Load Audio Generator (Edge TTS — instant, no model needed)
update_status("Loading Audio Generator (Edge TTS)...")
try:
from src.pro_audio_generator import process_script_to_audio
print("✅ Audio Generator Ready (Edge TTS)!", flush=True)
except Exception as e:
update_status(f"Critical Error: {e}")
print(f"❌ Failed to load audio generator: {e}", flush=True)
return
# Generators
gemini_gen = GeminiScriptGenerator()
local_gen = StoryGenerator()
if ENABLE_MUSIC_VIDEO_MODE:
update_status("Music Video Mode ENABLED")
print("\n🎵 Music Video Mode is ON", flush=True)
if not MUSIC_VIDEO_SONGS_FOLDER_ID or not MUSIC_VIDEO_VISUALS_FOLDER_ID:
print("❌ Please set DRIVE_SONGS_FOLDER and DRIVE_VISUALS_FOLDER in your environment.", flush=True)
return
while True:
try:
process_music_videos(MUSIC_VIDEO_SONGS_FOLDER_ID, MUSIC_VIDEO_VISUALS_FOLDER_ID, status_callback=update_status)
print("✅ Batch complete. Retrying in 1 hour...", flush=True)
time.sleep(3600)
except Exception as e:
print(f"❌ Music Video Error: {e}", flush=True)
time.sleep(60)
while True:
try:
update_status(f"Generating Script (Cycle #{VIDEO_COUNT + 1})...")
print(f"\n--- Starting New Generation Cycle #{VIDEO_COUNT + 1} ---")
# 1. Generate Script (Try Gemini First)
script_text = None
topic = "Self Improvement"
if gemini_gen.client:
print("🧠 Requesting AI Script from Gemini...", flush=True)
script_text, topic = gemini_gen.generate_script()
if not script_text or script_text.startswith("Error"):
update_status("Gemini Failed. Using Local Script.")
print("⚠️ Gemini Failed or API Key missing. Falling back to local scripts.", flush=True)
script_text, topic = local_gen.generate_story()
if not script_text:
update_status("No Script Found. Retrying...")
print("⚠️ No script generated (Gemini & Local failed). Retrying in 10s...")
time.sleep(10)
continue
# 2. Generate Audio (XTTS) - This takes time!
update_status(f"Generating Audio (Edge TTS) for '{topic}'...")
spoken_script = extract_spoken_script(script_text)
audio_path = process_script_to_audio(spoken_script, f"pro_audio_{int(time.time())}.mp3")
if not audio_path:
update_status("Audio Gen Failed. Retrying...")
print("⚠️ Audio generation failed. Retrying in 10s...")
time.sleep(10)
continue
# 3. Get Background from Pexels
update_status(f"Fetching Background Video (Pexels) for '{topic}'...")
bg_videos = asset_mgr.get_background_videos(topic, count=6)
if not bg_videos:
update_status("No Background Videos Found!")
print("⚠️ No background videos available. Waiting 10s...")
time.sleep(10)
continue
# 4. Render Video
video_path = create_video(audio_path, bg_videos, topic)
if video_path:
VIDEO_COUNT += 1
LAST_VIDEO = os.path.basename(video_path)
# 5. Upload to Drive
update_status(f"Uploading Video #{VIDEO_COUNT} to Drive...")
print(f"☁️ Uploading PRO Video #{VIDEO_COUNT} to Drive...")
drive_link = upload_to_drive(video_path)
if drive_link:
print(f"✅ Upload Complete: {drive_link}")
LAST_VIDEO_PATH = None
os.remove(video_path) # Cleanup Video
else:
print("⚠️ Upload Failed. Keeping local file.")
# Cleanup
if os.path.exists(audio_path): os.remove(audio_path)
# CRITICAL: Clean up downloaded background video
for bg_video in bg_videos:
if "backgrounds" in bg_video and os.path.exists(bg_video):
try:
os.remove(bg_video)
print(f"🧹 Cleaned up background video: {bg_video}", flush=True)
except: pass
# Delay
# Delay (5-10 mins)
cooldown = random.randint(300, 600)
mins = cooldown // 60
update_status(f"Cooling down ({mins}m)...")
print(f"⏳ Cooling down for {mins} minutes...", flush=True)
time.sleep(cooldown)
except Exception as e:
update_status(f"Global Error: {e}")
print(f"❌ Critical Loop Error: {e}", flush=True)
time.sleep(30)
# --- Gradio UI ---
def get_ui_status():
return CURRENT_STATUS, VIDEO_COUNT, LAST_VIDEO, LAST_VIDEO_PATH
def start_ui():
with gr.Blocks(title="AI Script Pro Generator", analytics_enabled=False) as app:
gr.Markdown("# 🚀 AI Long-Form Video Generator")
gr.Markdown("This space generates English self-improvement and psychology videos using AI scripts, Edge TTS, and cinematic landscape backgrounds.")
with gr.Row():
status_box = gr.Textbox(label="Current Activity", value="Initializing...", interactive=False)
count_box = gr.Number(label="Videos Generated", value=0, interactive=False)
with gr.Row():
last_vid_box = gr.Textbox(label="Last Video Produced", value="None", interactive=False)
video_output = gr.Video(label="Latest Video", interactive=False)
# Auto-refresh using Timer (Modern Gradio approach)
timer = gr.Timer(2)
timer.tick(fn=get_ui_status, outputs=[status_box, count_box, last_vid_box, video_output], api_name=False)
return app
if __name__ == "__main__":
# Start Generator in Background Thread
threading.Thread(target=main_loop, daemon=True).start()
# Launch UI on Port 7860
app = start_ui()
app.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False)