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)