Spaces:
Running
Running
File size: 34,523 Bytes
423659f a4528d8 423659f 9dce68e 79460e4 2db72f8 79460e4 2db72f8 79460e4 b8bb985 79460e4 9dce68e 79460e4 9dce68e a4528d8 423659f 9dce68e a4528d8 35a58b3 9dce68e a4528d8 35a58b3 79460e4 423659f 79460e4 bace241 79460e4 2db72f8 79460e4 423659f 79460e4 2db72f8 9dce68e 2db72f8 bace241 2db72f8 9dce68e 2db72f8 79460e4 b8bb985 a4528d8 b8bb985 79460e4 9dce68e 79460e4 9dce68e 79460e4 9dce68e 79460e4 2db72f8 79460e4 2db72f8 79460e4 9dce68e 79460e4 7087183 79460e4 2db72f8 79460e4 7087183 79460e4 7087183 79460e4 9dce68e 79460e4 2db72f8 79460e4 7087183 79460e4 7087183 79460e4 bace241 79460e4 bace241 9dce68e 79460e4 7087183 79460e4 bace241 79460e4 2db72f8 bace241 9dce68e 79460e4 bace241 79460e4 bace241 2db72f8 9dce68e 79460e4 bace241 79460e4 bace241 79460e4 bace241 9dce68e 79460e4 bace241 a4528d8 79460e4 2db72f8 bace241 9dce68e 79460e4 bace241 9dce68e 79460e4 bace241 f28f92b 9dce68e a4528d8 9dce68e 79460e4 9dce68e 79460e4 9dce68e 79460e4 9dce68e 2db72f8 bace241 9dce68e bace241 2db72f8 9dce68e 79460e4 9dce68e a4528d8 79460e4 9dce68e 79460e4 2db72f8 9dce68e 79460e4 9dce68e bace241 9dce68e 2db72f8 9dce68e bace241 9dce68e 2db72f8 9dce68e a4528d8 9dce68e bace241 2db72f8 a4528d8 9dce68e 79460e4 bace241 2db72f8 9dce68e bace241 9dce68e 7e3aba0 9dce68e 0576164 b8bb985 a4528d8 b8bb985 0576164 a4528d8 0576164 27329ae |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 |
# ==============================================================================
# engine.py - [FINAL FIXED VERSION]
# FIX: Removed 'zoompan' which was pausing videos. Restored natural video motion.
# ==============================================================================
import os
import time
import json
import uuid
import threading
import subprocess
import requests
import sqlite3
import random
import shutil
import re
from gtts import gTTS
from werkzeug.utils import secure_filename
# स्थानीय परीक्षण के लिए dotenv लोड करें (अगर उपलब्ध हो)
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
# ==============================================================================
# 1. Global Setup and Database Functions
# ==============================================================================
# प्रोजेक्ट की रूट डायरेक्टरी
APP_ROOT = '/code'
# सभी ज़रूरी फोल्डरों के लिए एब्सोल्यूट पाथ
DATA_FOLDER = os.path.join(APP_ROOT, 'data')
UPLOAD_FOLDER = os.path.join(APP_ROOT, 'uploads')
OUTPUT_FOLDER = os.path.join(APP_ROOT, 'outputs')
# डेटाबेस फाइल का पूरा एब्सोल्यूट पाथ
DATABASE_FILE = os.path.join(DATA_FOLDER, 'tasks.db')
def get_db_connection():
conn = sqlite3.connect(DATABASE_FILE, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db_connection()
conn.execute('CREATE TABLE IF NOT EXISTS tasks (id TEXT PRIMARY KEY, status TEXT NOT NULL, progress INTEGER NOT NULL, log TEXT, output_filename TEXT)')
conn.commit()
conn.close()
def create_task(task_id):
log_message = "मिशन शुरू हो रहा है...\n"
conn = get_db_connection()
conn.execute('INSERT INTO tasks (id, status, progress, log) VALUES (?, ?, ?, ?)', (task_id, 'processing', 0, log_message))
conn.commit()
conn.close()
def get_task(task_id):
conn = get_db_connection()
task = conn.execute('SELECT * FROM tasks WHERE id = ?', (task_id,)).fetchone()
conn.close()
return task
def update_task_log(task_id, message, progress):
conn = get_db_connection()
current_log = conn.execute('SELECT log FROM tasks WHERE id = ?', (task_id,)).fetchone()['log']
new_log = current_log + message + "\n"
conn.execute('UPDATE tasks SET log = ?, progress = ? WHERE id = ?', (new_log, progress, task_id))
conn.commit()
conn.close()
def update_task_final_status(task_id, status, error_message=None, output_filename=None):
conn = get_db_connection()
current_log = conn.execute('SELECT log FROM tasks WHERE id = ?', (task_id,)).fetchone()['log']
if status == 'error':
final_log = current_log + f"\n\n🚨 FATAL ERROR: {error_message}"
conn.execute('UPDATE tasks SET status = ?, log = ? WHERE id = ?', (status, final_log, task_id))
elif status == 'complete':
final_log = current_log + "🎉 मिशन पूरा हुआ!"
conn.execute('UPDATE tasks SET status = ?, progress = ?, output_filename = ?, log = ? WHERE id = ?', (status, 100, output_filename, final_log, task_id))
conn.commit()
conn.close()
def load_api_keys(prefix):
try:
prefix_lower = prefix.lower()
keys = [v.strip() for k, v in os.environ.items() if k.lower().startswith(prefix_lower) and v.strip()]
if keys:
print(f"✅ API Key Check: '{prefix}' के लिए {len(keys)} कीज़ मिलीं।")
else:
print(f"❌ API Key Check: '{prefix}' के लिए कोई कीज़ नहीं मिलीं!")
return keys
except Exception as e:
print(f"🚨 एनवायरनमेंट वेरिएबल्स लोड करते समय त्रुटि: {e}")
return []
# ==============================================================================
# 2. All API Classes
# ==============================================================================
class GroqAPI:
def __init__(self, api_keys):
self.api_keys, self.api_url, self.model, self._key_index = api_keys, "https://api.groq.com/openai/v1/audio/transcriptions", "whisper-large-v3", 0
def transcribe_audio(self, audio_path):
if not self.api_keys: raise Exception("Groq API key not found.")
api_key = self.api_keys[self._key_index % len(self.api_keys)]; self._key_index += 1
data = {'model': self.model, 'response_format': 'verbose_json', 'timestamp_granularities[]': 'word'}
headers = {'Authorization': f'Bearer {api_key}'}
try:
with open(audio_path, 'rb') as audio_file:
files = {'file': (os.path.basename(audio_path), audio_file, 'audio/mpeg')}; print(f"-> Groq API को शब्द-स्तर पर टाइमस्टैम्प के लिए भेजा जा रहा है...")
response = requests.post(self.api_url, headers=headers, data=data, files=files, timeout=120); response.raise_for_status()
words_data = response.json().get('words', []); print(f"-> ट्रांसक्रिप्शन सफल: {len(words_data)} शब्दों के टाइमस्टैम्प मिले।"); return words_data
except Exception as e: raise Exception(f"Groq API Error: {e}")
class PexelsAPI:
def __init__(self, api_keys):
if not api_keys: raise Exception("Pexels API key not found.")
self.api_key = api_keys[0]; self.api_url = "https://api.pexels.com/videos/search"
def search_and_download(self, query, download_path, orientation, search_page=1):
print(f"-> Pexels पर खोजा जा रहा है (Direct API): '{query}' (Page: {search_page}, Orientation: {orientation})")
headers = {'Authorization': self.api_key}; params = {'query': query, 'page': search_page, 'per_page': 1, 'orientation': orientation}
try:
response = requests.get(self.api_url, headers=headers, params=params, timeout=60); response.raise_for_status(); data = response.json()
if not data.get('videos'): print(f"-> Pexels पर '{query}' के लिए कोई परिणाम नहीं मिला।"); return None
video_data = data['videos'][0]; video_files = video_data.get('video_files', []); best_link = None
for video_file in video_files:
if video_file.get('quality') == 'hd': best_link = video_file.get('link'); break
if not best_link and video_files: best_link = video_files[0].get('link')
if not best_link: print(f"-> Pexels परिणाम में कोई डाउनलोड करने योग्य लिंक नहीं मिला।"); return None
print(f"-> Pexels से वीडियो डाउनलोड किया जा रहा है..."); download_response = requests.get(best_link, stream=True, timeout=60); download_response.raise_for_status()
with open(download_path, 'wb') as f:
for chunk in download_response.iter_content(chunk_size=8192): f.write(chunk)
print(f"-> सफलतापूर्वक सहेजा गया: {download_path}"); return download_path
except requests.exceptions.RequestException as e: print(f"🚨 Pexels API में त्रुटि: {e}"); return None
except Exception as e: print(f"🚨 Pexels वीडियो डाउनलोड करने में अज्ञात त्रुटि: {e}"); return None
class PixabayAPI:
def __init__(self, api_keys):
if not api_keys: raise Exception("Pixabay API key not found.")
self.api_key = api_keys[0]; self.api_url = "https://pixabay.com/api/videos/"
def search_and_download(self, query, download_path, orientation, max_clip_length, search_index=0):
print(f"-> Pixabay पर खोजा जा रहा है: '{query}' (Index: {search_index})")
params = {'key': self.api_key, 'q': query, 'per_page': 5, 'orientation': orientation, 'max_duration': int(max_clip_length)}
try:
response = requests.get(self.api_url, params=params, timeout=60); response.raise_for_status(); results = response.json()
if not results['hits'] or len(results['hits']) <= search_index: print(f"-> Pixabay पर '{query}' के लिए index {search_index} पर कोई परिणाम नहीं मिला।"); return None
video_url = results['hits'][search_index]['videos']['medium']['url']; print(f"-> Pixabay से वीडियो डाउनलोड किया जा रहा है...")
response = requests.get(video_url, stream=True, timeout=60); response.raise_for_status()
with open(download_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192): f.write(chunk)
print(f"-> सफलतापूर्वक सहेजा गया: {download_path}"); return download_path
except Exception as e: print(f"🚨 Pixabay API में त्रुटि: {e}"); return None
class GeminiTeam:
MODELS_LIST_URL = "https://generativelanguage.googleapis.com/v1beta/models"
def __init__(self, api_keys):
self.api_keys = api_keys
if not self.api_keys: raise Exception("Gemini API key not found.")
self.model_name = self._find_best_model()
if not self.model_name: raise Exception("Could not dynamically find a suitable Gemini 'flash' model from any of the provided keys.")
self.api_url = f"https://generativelanguage.googleapis.com/v1beta/{self.model_name}:generateContent"
print(f"✅ स्मार्ट मॉडल हंटर सफल: '{self.model_name}' का उपयोग किया जाएगा।")
def _find_best_model(self):
print("-> स्मार्ट मॉडल हंटर: सबसे अच्छे 'gemini-*-flash' मॉडल को खोजा जा रहा है...")
for api_key in self.api_keys:
try:
print(f"-> API Key के अंतिम 4 अक्षरों से कोशिश की जा रही है: ...{api_key[-4:]}")
response = requests.get(f"{self.MODELS_LIST_URL}?key={api_key}", timeout=20); response.raise_for_status(); data = response.json()
available_models = [m['name'] for m in data.get('models', []) if 'flash' in m['name'] and 'generateContent' in m.get('supportedGenerationMethods', []) and 'exp' not in m['name']]
if not available_models: continue
available_models.sort(reverse=True); print(f"-> उपलब्ध 'flash' मॉडल मिले: {available_models}"); return available_models[0]
except requests.exceptions.RequestException as e: print(f"🚨 API Key ...{api_key[-4:]} के साथ त्रुटि: {e}. अगली की आजमाई जा रही है..."); continue
print("🚨 स्मार्ट मॉडल हंटर में गंभीर त्रुटि: कोई भी Gemini API Key काम नहीं कर रही है।"); return None
def _make_resilient_api_call(self, prompt, timeout=120):
headers = {'Content-Type': 'application/json'}; payload = {'contents': [{'parts': [{'text': prompt}]}]}
for api_key in self.api_keys:
try:
print(f"-> Gemini को अनुरोध भेजा जा रहा है (Key: ...{api_key[-4:]}, Model: {self.model_name.split('/')[-1]})")
response = requests.post(f"{self.api_url}?key={api_key}", headers=headers, json=payload, timeout=timeout); response.raise_for_status(); result = response.json()
if 'candidates' not in result or not result['candidates']: print(f"🚨 चेतावनी: Key ...{api_key[-4:]} से कोई कैंडिडेट नहीं मिला (संभवतः सुरक्षा ब्लॉक)। अगली की आजमाई जा रही है..."); continue
return result
except requests.exceptions.RequestException as e: print(f"🚨 API कॉल में त्रुटि (Key: ...{api_key[-4:]}): {e}. अगली की आजमाई जा रही है..."); raise Exception("Gemini API Error: All available API keys failed. Please check your keys and quotas.")
def extract_keywords(self, script_text):
prompt = f"""You are a search query expert. Analyze the script below and for each scene, create a JSON object. Each object must contain: 1. "scene_description": A brief description of the scene. 2. "primary_query": A highly creative, emotional, and cinematic search query in English. This is the main attempt. 3. "fallback_query": A simple, literal, and direct search query in English. Use this if the primary query fails. RULES: - Your response MUST be ONLY a JSON list of objects. - All queries must be in English. Script: "{script_text}" Example: [ {{"scene_description": "A person looking at a mountain.", "primary_query": "inspirational mountain peak cinematic hope", "fallback_query": "man looking at mountain"}} ] Generate the JSON:"""
result = self._make_resilient_api_call(prompt)
json_str = result['candidates'][0]['content']['parts'][0]['text']
clean_str = json_str[json_str.find('['):json_str.rfind(']') + 1]; scenes = json.loads(clean_str)
try:
log_file_path = os.path.join(OUTPUT_FOLDER, 'gemini_analysis_log.json')
with open(log_file_path, 'w', encoding='utf-8') as f: json.dump(scenes, f, ensure_ascii=False, indent=4)
except Exception as e: print(f"🚨 चेतावनी: Gemini विश्लेषण लॉग करने में विफल: {e}")
print(f"-> Gemini ने सफलतापूर्वक {len(scenes)} प्राथमिक/फ़ॉलबैक दृश्य निकाले।"); return scenes
def create_master_timeline(self, word_timestamps, enriched_scenes_with_paths):
full_script_text = " ".join([word['word'] for word in word_timestamps]); total_duration = word_timestamps[-1]['end'] if word_timestamps else 0
# NOTE: Added instructions to avoid excessively long clips at the end
prompt = f"""You are an expert AI video editor. Create a frame-perfect timeline JSON. Assets: 1. **Full Script:** "{full_script_text}" 2. **Total Audio Duration:** {total_duration:.2f} seconds. 3. **Available Scene Clips:** {json.dumps(enriched_scenes_with_paths, indent=2)} 4. **Word-Level Timestamps (with Pauses):** {json.dumps(word_timestamps, indent=2)}. RULES: 1. Your response MUST be ONLY a list of JSON objects. 2. Each object must have "start", "end", "matched_clip", and "start_offset_seconds". 3. **CRITICAL:** The timeline MUST cover the entire audio duration from 0 to {total_duration:.2f} seconds. There should be NO GAPS. 4. **CRITICAL:** You MUST use each video from the 'Available Scene Clips' list only once. Do not repeat clips. 5. **PACING RULE:** Avoid making the last clip excessively long. Try to distribute the duration evenly if possible. 6. **PAUSE RULE:** Use '[PAUSE]' moments for transitions. Create the final timeline JSON:"""
result = self._make_resilient_api_call(prompt, timeout=180)
json_str = result['candidates'][0]['content']['parts'][0]['text']
clean_str = json_str[json_str.find('['):json_str.rfind(']') + 1]; final_timeline = json.loads(clean_str)
print(f"-> Gemini Master Editor ने सफलतापूर्वक {len(final_timeline)} क्लिप्स की टाइमलाइन और ऑफसेट बना दी है।"); return final_timeline
def generate_script(self, topic, video_length):
word_count_map = {"short": "~75 शब्द", "medium": "~150 शब्द", "long": "~300 शब्द"}; target_word_count = word_count_map.get(video_length, "~150 शब्द")
prompt = f"""आप 'स्पार्कलिंग ज्ञान' के लिए एक विशेषज्ञ हिंदी स्क्रिप्ट राइटर हैं। विषय: "{topic}". निर्देश: 1. इस विषय पर एक आकर्षक, {target_word_count} की स्क्रिप्ट लिखें। 2. भाषा सरल और बोलचाल वाली हो। 3. हर 2-3 लाइनों के बाद एक नया विज़ुअल या सीन दिखाया जा सके, इस तरह से लिखें। 4. **CRITICAL RULE:** आपका आउटपुट सिर्फ और सिर्फ बोले जाने वाले डायलॉग्स (narration) होने चाहिए। किसी भी तरह के विज़ुअल निर्देश, सीन डिस्क्रिप्शन या ब्रैकेट () [] में लिखी कोई भी जानकारी आउटपुट में नहीं होनी चाहिए। सिर्फ वो टेक्स्ट दें जो ऑडियो में बोला जाएगा। अब, स्क्रिप्ट लिखें:"""
result = self._make_resilient_api_call(prompt)
generated_script = result['candidates'][0]['content']['parts'][0]['text']
print("-> Gemini ने सफलतापूर्वक स्क्रिप्ट जेनरेट कर दी है।"); return generated_script.strip()
# ==============================================================================
# 3. Enhanced Video Assembler (FIXED: NO FREEZE)
# ==============================================================================
class VideoAssembler:
# Cinematic Transitions Setup
TRANSITION_DURATION = 0.75
TRANSITION_TYPES = ['fade', 'wipeleft', 'wiperight', 'slideup', 'slidedown', 'circleopen', 'rectcrop']
def __init__(self, timeline, narration_audio, output_path, width, height, mute_audio, temp_dir):
self.timeline = timeline
self.narration_audio = narration_audio
self.output_path = output_path
self.width = width
self.height = height
self.mute_audio = mute_audio
self.temp_dir = temp_dir
def _run_ffmpeg_command(self, command, suppress_errors=False):
process = subprocess.run(command, capture_output=True, text=True)
if not suppress_errors and process.returncode != 0:
error_details = f"Return Code {process.returncode}"
if process.returncode == -9: error_details += " (SIGKILL): Process killed (Memory/CPU limit)."
raise Exception(f"FFmpeg Error ({error_details}):\nSTDERR:\n{process.stderr}")
return process
def assemble_video(self, log_callback):
if not self.timeline: return
# --- STAGE 1: Clip Preparation (Motion Restored) ---
log_callback("-> Stage 1/3: क्लिप्स तैयार की जा रही हैं (Normal Motion)...", 91)
prepared_clips = []
for i, item in enumerate(self.timeline):
input_clip_path = item['matched_clip']
# क्लिप की अवधि पता करना
try:
ffprobe_cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', input_clip_path]
duration_proc = self._run_ffmpeg_command(ffprobe_cmd)
actual_clip_duration = float(duration_proc.stdout.strip())
except Exception as e:
log_callback(f"🚨 Skipping clip due to error: {e}", 91); continue
start_offset = float(item.get('start_offset_seconds', 0.0))
if start_offset >= actual_clip_duration: start_offset = 0.0
is_last_clip = (i == len(self.timeline) - 1)
overlap = 0 if is_last_clip else self.TRANSITION_DURATION
duration = (float(item['end']) - float(item['start'])) + overlap
if duration <= 0: continue
output_clip_path = os.path.join(self.temp_dir, f"prepared_{i:03d}.mp4")
# 🔥 FIX: No 'zoompan'. Use scale and crop to keep video moving.
# scale='w=WIDTH:h=HEIGHT:force_original_aspect_ratio=increase' ensures cover
# crop='WIDTH:HEIGHT' centers the video
command = [
'ffmpeg', '-y', '-ss', str(start_offset), '-i', input_clip_path, '-t', str(duration),
'-vf', f"scale='w={self.width}:h={self.height}:force_original_aspect_ratio=increase',crop={self.width}:{self.height},setsar=1,fps=30",
'-c:v', 'libx264', '-preset', 'medium', '-crf', '23', '-an', '-threads', '2',
output_clip_path
]
self._run_ffmpeg_command(command)
prepared_clips.append(output_clip_path)
# --- STAGE 2: Merging with Random Transitions ---
log_callback("-> Stage 2/3: रैंडम ट्रांजिशन्स (Random Transitions) के साथ जोड़ा जा रहा है...", 94)
if not prepared_clips: raise Exception("No clips prepared.")
if len(prepared_clips) == 1:
shutil.copy(prepared_clips[0], self.output_path)
transitioned_video_path = self.output_path
else:
current_video = prepared_clips[0]
for i in range(len(prepared_clips) - 1):
next_video = prepared_clips[i+1]
output_path = os.path.join(self.temp_dir, f"transition_{i:03d}.mp4")
# 🔥 Random Transition Selection
trans_type = random.choice(self.TRANSITION_TYPES)
log_callback(f" -> Adding '{trans_type}' between clip {i+1} & {i+2}", 95)
ffprobe_cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', current_video]
duration_proc = self._run_ffmpeg_command(ffprobe_cmd)
curr_duration = float(duration_proc.stdout.strip())
offset = curr_duration - self.TRANSITION_DURATION
command = [
'ffmpeg', '-y', '-i', current_video, '-i', next_video,
'-filter_complex', f"[0:v][1:v]xfade=transition={trans_type}:duration={self.TRANSITION_DURATION}:offset={offset},format=yuv420p",
'-c:v', 'libx264', '-preset', 'medium', '-crf', '23', output_path
]
self._run_ffmpeg_command(command)
current_video = output_path
transitioned_video_path = current_video
# --- STAGE 3: Final Audio Mixing ---
log_callback("-> Stage 3/3: फाइनल ऑडियो मिक्सिंग और रेंडरिंग...", 98)
audio_input = [] if self.mute_audio else ['-i', self.narration_audio]
audio_map = ['-an'] if self.mute_audio else ['-map', '0:v:0', '-map', '1:a:0']
command = [
'ffmpeg', '-y', '-i', transitioned_video_path,
] + audio_input + [
'-c:v', 'copy',
] + audio_map + [
'-c:a', 'aac', '-shortest', self.output_path
]
self._run_ffmpeg_command(command)
# ==============================================================================
# 4. Main Worker Logic
# ==============================================================================
def run_ai_engine_worker(task_id, script_text, script_file_path, orientation, max_clip_length, mute_final_video):
log = lambda message, progress: update_task_log(task_id, message, progress)
temp_dir = os.path.join(UPLOAD_FOLDER, task_id)
try:
log("Step 0: API Keys की पुष्टि...", 2)
gemini_keys = load_api_keys("Gemini_Key")
pexels_keys = load_api_keys("Pexels_Key")
pixabay_keys = load_api_keys("Pixabay_Key")
groq_keys = load_api_keys("Groq_Key")
missing = []
if not gemini_keys: missing.append("Gemini_Key")
if not pexels_keys: missing.append("Pexels_Key")
if not pixabay_keys: missing.append("Pixabay_Key")
if not groq_keys: missing.append("Groq_Key")
if missing:
raise Exception(f"API Key Error: ये कीज़ नहीं मिलीं: {', '.join(missing)}")
gemini = GeminiTeam(api_keys=gemini_keys); log("-> सभी जरूरी API कीज मौजूद हैं।", 5)
log("Step 1: स्क्रिप्ट तैयार की जा रही है...", 8)
os.makedirs(temp_dir, exist_ok=True); narration_audio_path = ""
if script_file_path:
narration_audio_path = script_file_path; log("-> ऑडियो फ़ाइल प्राप्त हुई।", 12)
else:
cleaned_script_for_tts = re.sub(r'\[.*?\]|\(.*?\)', '', script_text)
full_script_text_for_tts = cleaned_script_for_tts.strip()
narration_audio_path = os.path.join(temp_dir, "narration.mp3")
gTTS(full_script_text_for_tts, lang='hi').save(narration_audio_path)
log(f"-> TTS ke liye saaf script bheji gayi: '{full_script_text_for_tts}'", 12)
log("-> टेक्स्ट से ऑडियो सफलतापूर्वक बनाया गया।", 12)
log("Step 2: ऑडियो का सटीक विश्लेषण (Groq)...", 15)
groq_api = GroqAPI(api_keys=groq_keys); word_timestamps = groq_api.transcribe_audio(narration_audio_path)
if not word_timestamps: raise Exception("Transcription failed or returned no words.")
log("-> Smart Pause Detector: ऑडियो में लंबी chuppi खोजी जा रही है...", 20)
timestamps_with_pauses = []; pause_threshold = 1.5; pause_count = 0
if word_timestamps:
timestamps_with_pauses.append(word_timestamps[0])
for i in range(len(word_timestamps) - 1):
current_word_end = float(word_timestamps[i]['end']); next_word_start = float(word_timestamps[i+1]['start'])
gap = next_word_start - current_word_end
if gap > pause_threshold:
pause_event = {'word': '[PAUSE]', 'start': current_word_end, 'end': next_word_start}
timestamps_with_pauses.append(pause_event); pause_count += 1
timestamps_with_pauses.append(word_timestamps[i+1])
if pause_count > 0: log(f"-> Pause Detector ne सफलतापूर्वक {pause_count} pauses jode.", 22)
else: log("-> Pause Detector ko koi lamba pause nahi mila. Sab theek hai.", 22)
full_script_text = " ".join([word['word'] for word in timestamps_with_pauses]); log(f"-> पूर्ण स्क्रिप्ट: '{full_script_text}'", 25)
log("Step 3: Contextual वीडियो खोजे जा रहे हैं (Smart Search)...", 30)
scenes_from_gemini = gemini.extract_keywords(full_script_text)
pexels = PexelsAPI(api_keys=pexels_keys); pixabay = PixabayAPI(api_keys=pixabay_keys)
for i, scene in enumerate(scenes_from_gemini):
scene['downloaded_path'] = None; primary_query = scene.get('primary_query'); fallback_query = scene.get('fallback_query')
log(f"-> Scene {i+1} ('{scene['scene_description'][:20]}...') के लिए वीडियो खोजा जा रहा है...", 30 + i * 5)
filename = f"scene_{i+1}_{secure_filename(primary_query)[:20]}.mp4"; download_path = os.path.join(temp_dir, filename)
log(f" -> प्राथमिक कोशिश (Primary): '{primary_query}'...", 30 + i * 5)
for attempt in range(2):
path = pexels.search_and_download(primary_query, download_path, orientation, search_page=attempt + 1)
if path: scene['downloaded_path'] = path; break
path = pixabay.search_and_download(primary_query, download_path, orientation, max_clip_length, search_index=attempt)
if path: scene['downloaded_path'] = path; break
if not scene.get('downloaded_path'):
log(f" -> प्राथमिक कोशिश विफल। फ़ॉलबैक कोशिश (Fallback): '{fallback_query}'...", 30 + i * 5)
for attempt in range(2):
path = pexels.search_and_download(fallback_query, download_path, orientation, search_page=attempt + 1)
if path: scene['downloaded_path'] = path; break
path = pixabay.search_and_download(fallback_query, download_path, orientation, max_clip_length, search_index=attempt)
if path: scene['downloaded_path'] = path; break
if scene['downloaded_path']: log(f"-> Scene {i+1} के लिए वीडियो मिला: {os.path.basename(scene['downloaded_path'])}", 30 + i * 5)
else: log(f"🚨 चेतावनी: Scene {i+1} के लिए कोई भी वीडियो नहीं मिला।", 30 + i * 5)
successful_scenes = [scene for scene in scenes_from_gemini if scene.get('downloaded_path')]
if not successful_scenes: raise Exception("Could not download any videos for the given script.")
log(f"-> {len(successful_scenes)} वीडियो क्लिप्स सफलतापूर्वक डाउनलोड हुए।", 60)
log("Step 4: मास्टर टाइमलाइन और इंटेलिजेंट क्लिप ट्रिमिंग...", 75)
final_timeline = gemini.create_master_timeline(timestamps_with_pauses, successful_scenes); log(f"-> मास्टर टाइमलाइन AI से प्राप्त हुई।", 85)
validated_timeline = []
used_clips = set()
for clip in final_timeline:
path_value = clip.get('matched_clip'); actual_path = None
if isinstance(path_value, str): actual_path = path_value
elif isinstance(path_value, dict): actual_path = path_value.get('downloaded_path')
if actual_path and isinstance(actual_path, str) and os.path.exists(actual_path) and actual_path not in used_clips:
clip['matched_clip'] = actual_path; validated_timeline.append(clip); used_clips.add(actual_path)
else: log(f"🚨 चेतावनी: टाइमलाइन में अमान्य क्लिप: {os.path.basename(actual_path or 'Invalid')}", 87)
if not validated_timeline: raise Exception("Timeline verification failed.")
log("-> टाइमलाइन में गैप्स की जाँच...", 88)
final_gapless_timeline = []; total_duration = word_timestamps[-1]['end'] if word_timestamps else 0
validated_timeline.sort(key=lambda x: float(x['start']))
for i, clip in enumerate(validated_timeline):
if i < len(validated_timeline) - 1:
current_clip_end = float(clip['end']); next_clip_start = float(validated_timeline[i+1]['start'])
if current_clip_end < next_clip_start: clip['end'] = next_clip_start
elif i == len(validated_timeline) - 1:
last_clip_end = float(clip['end'])
if last_clip_end < total_duration: clip['end'] = total_duration
final_gapless_timeline.append(clip)
log("Step 5: फाइनल वीडियो को रेंडर किया जा रहा है (Natural Motion, No Freeze)...", 90)
width, height = (1080, 1920) if orientation == 'vertical' else (1920, 1080)
output_filename = f"{task_id}_final_video.mp4"; output_path = os.path.join(OUTPUT_FOLDER, output_filename)
assembler = VideoAssembler(final_gapless_timeline, narration_audio_path, output_path, width, height, mute_final_video, temp_dir)
assembler.assemble_video(log)
log("-> अंतिम विस्तृत रिपोर्ट बनाई जा रही है...", 99)
try:
report_data = {
"full_transcribed_script": full_script_text,
"groq_word_timestamps": word_timestamps,
"timestamps_with_pauses_added": timestamps_with_pauses,
"gemini_scene_analysis_and_downloads": successful_scenes,
"processed_gapless_timeline": final_gapless_timeline,
}
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: log(f"🚨 चेतावनी: विस्तृत रिपोर्ट सहेजने में विफल: {e}", 99)
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); log(f"-> Temp files cleaned.", 100)
except Exception as e: print(f"Cleanup Error: {e}")
def generate_script_with_ai(topic, video_length):
print(f"-> AI स्क्रिप्ट जेनरेटर शुरू हुआ: Topic='{topic}', Length='{video_length}'")
try:
gemini_keys = load_api_keys("Gemini_Key")
if not gemini_keys: raise Exception("Gemini API key not found for script generation.")
gemini_agent = GeminiTeam(api_keys=gemini_keys)
script = gemini_agent.generate_script(topic, video_length)
return script
except Exception as e: raise e |