|
|
| 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_dotenv() |
|
|
| |
| 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 |
| |
|
|
| |
| 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() |
|
|
| |
| 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_mgr = AssetManager() |
|
|
| |
| 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 |
| ] |
| |
| |
| 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() |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| 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 |
|
|
| |
| def main_loop(): |
| global VIDEO_COUNT, LAST_VIDEO, LAST_VIDEO_PATH |
| update_status("Starting Generator Loop...") |
| |
| |
| time.sleep(5) |
| |
| |
| 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 |
|
|
| |
| 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} ---") |
| |
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| video_path = create_video(audio_path, bg_videos, topic) |
| |
| if video_path: |
| VIDEO_COUNT += 1 |
| LAST_VIDEO = os.path.basename(video_path) |
| |
| |
| 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) |
| else: |
| print("⚠️ Upload Failed. Keeping local file.") |
|
|
| |
| if os.path.exists(audio_path): os.remove(audio_path) |
| |
| |
| 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 |
|
|
| |
| |
| 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) |
|
|
| |
| 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) |
| |
| |
| 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__": |
| |
| threading.Thread(target=main_loop, daemon=True).start() |
| |
| |
| app = start_ui() |
| app.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False) |
|
|