# engine.py - [HYBRID ARCHITECTURE EDITION: GLOBAL VISION + ROLLING BATCHES] import os import json import random import shutil import re import time import concurrent.futures import subprocess try: from dotenv import load_dotenv load_dotenv() except ImportError: pass from core.constants import ( UPLOAD_FOLDER, OUTPUT_FOLDER, BGM_FOLDER, SFX_FOLDER, DEFAULT_VERTICAL_RES, DEFAULT_HORIZONTAL_RES, MAX_CLIPS_TO_CHECK_SETTING, PASSING_SCORE_SETTING, CINEMATIC_ART_STYLE ) from core.database import init_db, create_task, get_task, update_task_log, update_task_final_status from core.utils import load_api_keys, ensure_hindi_font from core.api_clients import ( AwaazAPI, GroqAPI, HuggingFacePNGAPI, CustomImageGenAPI, MetaSparkStudioAPI, StockClipAPI ) from core.gemini_core import GeminiTeam from core.media_workers import ( process_all_scenes_parallel, build_master_audio_mix, generate_cinematic_subtitles, VideoAssembler, fast_concat_chunk_videos ) def run_ai_engine_worker(task_id, script_text, script_file_path, orientation, story_mode=False, *args, **kwargs): log = lambda message, progress: update_task_log(task_id, message, progress) temp_dir = os.path.join(UPLOAD_FOLDER, task_id) os.makedirs(temp_dir, exist_ok=True) try: log("Step 0: API Keys की पुष्टि...", 2) gemini_keys = load_api_keys("gmni") groq_keys = load_api_keys("groq") hf_space_name = os.environ.get("HF_SPACE_NAME") hf_master_key = os.environ.get("MY_MASTER_KEY") awaaz_api_key = os.environ.get("AWAAZ_API_KEY") imgen_api_key = os.environ.get("IMGEN_API_KEY") mss_api_key = os.environ.get("META_API_KEY") stockclip_api_key = os.environ.get("STOCKCLIP_API_KEY") missing = [name for key, name in [ (gemini_keys, "gmni"), (groq_keys, "groq"), (hf_space_name, "HF Space"), (awaaz_api_key, "Awaaz"), (mss_api_key, "Meta API"), (imgen_api_key, "Sparkling Image API"), (stockclip_api_key, "StockClip API") ] if not key] if missing: raise Exception(f"API Key Error: ये कीज़ नहीं मिले: {', '.join(missing)}") gemini = GeminiTeam(api_keys=gemini_keys) png_api = HuggingFacePNGAPI(hf_space_name, hf_master_key) awaaz = AwaazAPI(awaaz_api_key) groq_api = GroqAPI(api_keys=groq_keys) stockclip_api = StockClipAPI(stockclip_api_key) stockclip_api.set_logger(log) image_gen_client = CustomImageGenAPI(imgen_api_key) mss_api_client = MetaSparkStudioAPI(mss_api_key) mss_api_client.set_logger(log) log("Phase 1: 'Global Overseer' (Visual Bible) तैयार किया जा रहा है...", 5) is_raw_script_available = bool(script_text and script_text.strip()) if script_file_path and not is_raw_script_available: log("-> 🎧 ऑडियो फ़ाइल डिटेक्ट हुई! Flash Lite से ट्रांसक्रिप्ट कर रहे हैं...", 10) prelim_scenes = gemini.native_audio_scene_cutter(script_file_path) script_text = " ".join([s.get('text', '') for s in prelim_scenes]) is_raw_script_available = True global_context = gemini.generate_global_context(script_text, "Mystery / Space / Folklore") global_summary = global_context if not global_summary.get('core_theme'): global_summary['core_theme'] = 'Cinematic' producer_rules = gemini.get_chief_producer_rules(script_text, is_raw_script_available) # ============================================================================== # 🎙️ THE SINGLE PASS AUDIO GENERATION # ============================================================================== log("Phase 2: 🎙️ Awaaz API से पूरी स्क्रिप्ट का सिंगल मास्टर ऑडियो बन रहा है...", 15) full_audio_path = os.path.join(temp_dir, f"{task_id}_master_audio.wav") cleaned_script = re.sub(r'\[.*?\]|\(.*?\)', '', script_text).strip() enhanced_script = awaaz.enhance_script(cleaned_script) if not enhanced_script.startswith('['): enhanced_script = f"[male] {enhanced_script}" awaaz.generate_audio(enhanced_script, full_audio_path) # ============================================================================== # ✂️ FLASH LITE MASTER CUTTER (WITH SMART RETRY LOGIC) # ============================================================================== log("Phase 3: ✂️ Flash Lite मास्टर ऑडियो को सीन्स में काट रहा है...", 25) MAX_RETRIES = 3 all_raw_scenes = [] transcription_success = False for attempt in range(MAX_RETRIES): try: log(f"-> 🎙️ ट्रांसक्रिप्शन का प्रयास {attempt + 1}/{MAX_RETRIES}...", 26) all_raw_scenes = gemini.native_audio_scene_cutter(full_audio_path) # 🛡️ THE STRICT VALIDATOR (चेक करो कि कहीं API ने खाली तो नहीं भेज दिया) is_timestamp_broken = all(s.get('start_time', 0.0) == 0.0 and s.get('end_time', 0.0) == 0.0 for s in all_raw_scenes) is_text_empty = all(not s.get('text', '').strip() for s in all_raw_scenes) if not is_timestamp_broken and not is_text_empty and len(all_raw_scenes) > 0: transcription_success = True log("-> ✅ ट्रांसक्रिप्शन सफल रहा! टाइमस्टैम्प्स और टेक्स्ट मिल गए।", 27) break # अगर सब सही है, तो लूप से बाहर आ जाओ else: log(f"⚠️ चेतावनी: API ने खाली या 0.0 टाइमस्टैम्प वाला डेटा दिया है। (Attempt {attempt + 1} Failed)", 27) time.sleep(3) # API को साँस लेने का टाइम दो except Exception as e: log(f"🚨 ट्रांसक्रिप्शन API क्रैश हो गया: {e}", 27) time.sleep(3) # अगर 3 बार के बाद भी API ने कचरा ही दिया, तो इंजन को रोक दो (Fail Fast) if not transcription_success: raise Exception("❌ FATAL ERROR: 3 प्रयासों के बाद भी ट्रांसक्रिप्शन API सही डेटा नहीं दे पाया। कृपया API Key या ऑडियो फाइल चेक करें।") # अगर सब सही है, तो हमारा गैप-फिक्स (Gapless Timeline) चलाओ for i in range(len(all_raw_scenes) - 1): all_raw_scenes[i]['end_time'] = all_raw_scenes[i+1]['start_time'] # ============================================================================== # 🌍 THE GOD'S EYE VIEW (Global Visual Assignment) # ============================================================================== log("Phase 4: 🧠 Master Director पूरे 100+ सीन्स को एक साथ देख रहा है (Global Reuse Logic)...", 35) gemini.global_summary_cache = global_summary all_assigned_scenes = gemini.assign_visuals_to_scenes(all_raw_scenes, producer_rules, past_clips=None) if not all_assigned_scenes: raise Exception("Master Director failed to assign visuals globally.") # ============================================================================== # 📦 BATCH PREPARATION & GLOBAL VAULT (Saving RAM and API Output Limits) # ============================================================================== BATCH_SIZE = 12 scene_batches = [all_assigned_scenes[i:i + BATCH_SIZE] for i in range(0, len(all_assigned_scenes), BATCH_SIZE)] log(f"-> 📦 {len(all_assigned_scenes)} सीन्स को {len(scene_batches)} बैचेस में बाँटा गया है (सुरक्षित प्रोसेसिंग के लिए)।", 40) # 💡 यहाँ हमारी ग्लोबल तिजोरी (Vault) बन रही है, जो पूरे 11 मिनट के वीडियो की मेमोरी रखेगी # 💡 The 3-Tier Enterprise Vault System GLOBAL_CLIP_VAULT = {} # SceneID -> Path GLOBAL_SIGNATURE_INDEX = {} # Signature -> SceneID GLOBAL_DNA_VAULT = {} # SceneID -> Raw DNA (For future embeddings chunk_video_paths = [] all_master_timestamps = [] all_successful_scenes = [] all_gapless_timelines = [] past_prompts_history = [] width, height = DEFAULT_VERTICAL_RES if orientation == 'vertical' else DEFAULT_HORIZONTAL_RES # ============================================================================== # 🔄 BATCH PROCESSING LOOP # ============================================================================== for batch_idx, batch_scenes in enumerate(scene_batches): current_progress = 40 + int((batch_idx / len(scene_batches)) * 40) log(f"-> 🔄 Processing Batch {batch_idx + 1}/{len(scene_batches)}...", current_progress) batch_timestamps = [{'word': s.get('text', ''), 'start': s.get('start_time', 0.0), 'end': s.get('end_time', 0.0)} for s in batch_scenes] all_master_timestamps.extend(batch_timestamps) batch_data_for_prompt = [{"id": str(i), "desc": s.get('emotion_and_metaphor', s.get('script_line', ''))} for i, s in enumerate(batch_scenes)] enhanced_results = gemini.enhance_batch_cinematic_prompts(batch_data_for_prompt, global_summary.get('core_theme', 'Cinematic'), CINEMATIC_ART_STYLE, past_prompts_history[-5:]) for i, s in enumerate(batch_scenes): prompt = enhanced_results.get(str(i)) or f"{s.get('emotion_and_metaphor', s.get('script_line', ''))}, {CINEMATIC_ART_STYLE}" s['cinematic_prompt'] = prompt past_prompts_history.append(prompt) base_scene_config = { 'temp_dir': temp_dir, 'orientation': orientation, 'story_mode': story_mode, 'global_context': global_context, 'stockclip_api': stockclip_api, 'png_api': png_api, 'gemini': gemini, 'groq_api': groq_api, 'video_api': mss_api_client, 'image_api': image_gen_client, 'mss_api': mss_api_client, 'thread_safe_log': log, 'MAX_CLIPS_TO_CHECK': MAX_CLIPS_TO_CHECK_SETTING, 'PASSING_SCORE': PASSING_SCORE_SETTING, 'chunk_idx': batch_idx, # 💡 तिजोरी की चाबी मज़दूरों को दे दी गई है 'clip_vault': GLOBAL_CLIP_VAULT, 'signature_index': GLOBAL_SIGNATURE_INDEX, 'dna_vault': GLOBAL_DNA_VAULT } processed_batch_scenes = process_all_scenes_parallel(batch_scenes, base_scene_config) successful_batch_scenes = [res for res in processed_batch_scenes if res and (os.path.exists(res.get('downloaded_path', '')) or res.get('downloaded_path') == 'REUSE_PLACEHOLDER')] if not successful_batch_scenes: continue all_successful_scenes.extend(successful_batch_scenes) # ✅ AI की जगह Pure Python Mapping (No Hallucination) validated_timeline = [] for i, ts in enumerate(batch_timestamps): # processed_batch_scenes और batch_timestamps का इंडेक्स हमेशा समान (Parallel) रहता है scene_data = processed_batch_scenes[i] if i < len(processed_batch_scenes) else None if scene_data and scene_data.get('downloaded_path') and os.path.exists(scene_data['downloaded_path']): validated_timeline.append({ "scene_index": i + 1, "word": ts['word'], "start": ts['start'], "end": ts['end'], "matched_clip": scene_data['downloaded_path'], "start_offset_seconds": 0.0 }) if not validated_timeline: continue all_gapless_timelines.extend(validated_timeline) # 🎬 AI TRANSITIONS timeline_context_for_ai = [{"line": s.get('script_line', ''), "vibe": s.get('emotion_and_metaphor', '')} for s in successful_batch_scenes] ai_transitions = gemini.get_dynamic_transitions(timeline_context_for_ai) chunk_output_path = os.path.join(temp_dir, f"chunk_{batch_idx}_silent.mp4") assembler = VideoAssembler(validated_timeline, chunk_output_path, width, height, temp_dir, ai_transitions) prepared_data_ordered = [None] * len(validated_timeline) with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor: prep_futures = {executor.submit(assembler.prepare_clip_parallel, i, clip): i for i, clip in enumerate(validated_timeline)} for future in concurrent.futures.as_completed(prep_futures): idx = prep_futures[future] prepared_data_ordered[idx] = future.result() # 🔇 THE MAGIC: Passing None for audio and subs to create a SILENT CHUNK assembler.assemble_final_video(prepared_data_ordered, None, None, log) if os.path.exists(chunk_output_path): chunk_video_paths.append(chunk_output_path) # ============================================================================== # 🔗 THE ULTIMATE FEVICOL (Fast Concat of Silent Chunks) # ============================================================================== if not chunk_video_paths: raise Exception("Media Generation Failed. कोई भी वीडियो चंक नहीं बन पाया।") log("Step Final: 🚀 सारे साइलेंट चंक्स को 'महा-फेविकोल' से जोड़ा जा रहा है...", 85) master_silent_video = os.path.join(temp_dir, f"{task_id}_master_silent.mp4") fast_concat_chunk_videos(chunk_video_paths, master_silent_video, log) # ============================================================================== # 🎵 THE GRAND FINALE (Master Audio, SFX, BGM & Subtitles) # ============================================================================== log("-> 🎵 मास्टर ऑडियो, बैकग्राउंड म्यूज़िक और सिनेमैटिक सबटाइटल्स लगाए जा रहे हैं...", 90) # 1. Master Audio Design (BGM/SFX for the ENTIRE video at once) total_duration_full = float(all_master_timestamps[-1]['end']) if all_master_timestamps else 0 transition_times_full = [float(clip['start']) for clip in all_gapless_timelines[1:]] available_bgms = [f for f in os.listdir(BGM_FOLDER) if f.endswith(('.mp3', '.wav'))] available_sfxs = [f for f in os.listdir(SFX_FOLDER) if f.endswith(('.mp3', '.wav'))] timeline_context_full = [{"line": s.get('script_line', ''), "vibe": s.get('emotion_and_metaphor', '')} for s in all_successful_scenes] audio_design = {"bgm": [], "sfx": []} if available_bgms: audio_design = gemini.get_audio_design(timeline_context_full, available_bgms, available_sfxs, total_duration_full, transition_times_full) if available_sfxs and transition_times_full: perfect_sfx = [{"file": random.choice(available_sfxs), "time": t} for t in transition_times_full] audio_design["sfx"] = perfect_sfx final_mixed_audio_path = os.path.join(temp_dir, f"{task_id}_master_mixed.wav") build_master_audio_mix(full_audio_path, audio_design, BGM_FOLDER, SFX_FOLDER, final_mixed_audio_path) # 2. Master Subtitles (For the ENTIRE video at once) final_subs_path = os.path.join(temp_dir, f"{task_id}_master_subs.ass") dummy_assembler = VideoAssembler([], "", width, height, temp_dir, []) dummy_assembler.font_dir, font_family_name = ensure_hindi_font() generate_cinematic_subtitles(all_master_timestamps, final_subs_path, width, height, font_family_name) # 3. The Ultimate Merge (Silent Video + Mixed Audio + Subtitles -> Final Video) output_filename = f"{task_id}_final_video.mp4" final_output_path = os.path.join(OUTPUT_FOLDER, output_filename) ff_subs_path = final_subs_path.replace('\\', '/').replace(':', '\\:') ff_font_dir = getattr(dummy_assembler, 'font_dir', '').replace('\\', '/').replace(':', '\\:') ass_filter = f"ass='{ff_subs_path}':fontsdir='{ff_font_dir}'" if ff_font_dir else f"ass='{ff_subs_path}'" final_merge_cmd = [ 'ffmpeg', '-y', '-i', master_silent_video, '-i', final_mixed_audio_path, '-vf', ass_filter, '-c:v', 'libx264', '-preset', 'faster', '-crf', '24', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '128k', '-shortest', '-movflags', '+faststart', '-threads', '0', final_output_path ] subprocess.run(final_merge_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) log("-> 📝 अंतिम विस्तृत JSON रिपोर्ट बनाई जा रही है...", 98) try: full_transcribed_script = " ".join([w['word'] for w in all_master_timestamps]) report_data = { "producer_rules": producer_rules, "full_transcribed_script": full_transcribed_script, "flash_scene_timestamps": all_master_timestamps, "gemini_scene_analysis_and_downloads": all_successful_scenes, "processed_gapless_timeline": all_gapless_timelines } report_file_path = os.path.join(OUTPUT_FOLDER, f'{task_id}_report.json') with open(report_file_path, 'w', encoding='utf-8') as f: json.dump(report_data, f, ensure_ascii=False, indent=4) except Exception as e: pass log("-> ✅ वीडियो जनरेशन 100% पूरा हुआ!", 100) update_task_final_status(task_id, 'complete', output_filename=output_filename) except Exception as e: import traceback traceback.print_exc() update_task_final_status(task_id, 'error', error_message=str(e)) finally: if os.path.exists(temp_dir): try: shutil.rmtree(temp_dir, ignore_errors=True) except: pass # ============================================================================== # 🤖 THE MISSING FUNCTION RESTORED # ============================================================================== def generate_script_with_ai(topic, video_length): try: gemini_keys = load_api_keys("gmni") if not gemini_keys: raise Exception("Gemini API keys not found for script generation.") return GeminiTeam(api_keys=gemini_keys).generate_script(topic, video_length) except Exception as e: raise e