Spaces:
Running
Running
Update engine.py
Browse files
engine.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ==============================================================================
|
| 2 |
-
# engine.py - [FINAL
|
| 3 |
-
#
|
| 4 |
# ==============================================================================
|
| 5 |
|
| 6 |
import os
|
|
@@ -28,15 +28,15 @@ except ImportError:
|
|
| 28 |
# 1. Global Setup and Database Functions
|
| 29 |
# ==============================================================================
|
| 30 |
|
| 31 |
-
# प्रोजेक्ट की रूट डायरेक्टरी
|
| 32 |
APP_ROOT = '/code'
|
| 33 |
|
| 34 |
-
# सभी ज़रूरी फोल्डरों के लिए एब्सोल्यूट पाथ
|
| 35 |
DATA_FOLDER = os.path.join(APP_ROOT, 'data')
|
| 36 |
UPLOAD_FOLDER = os.path.join(APP_ROOT, 'uploads')
|
| 37 |
OUTPUT_FOLDER = os.path.join(APP_ROOT, 'outputs')
|
| 38 |
|
| 39 |
-
# डेटाबेस फाइल का पूरा एब्सोल्यूट पाथ
|
| 40 |
DATABASE_FILE = os.path.join(DATA_FOLDER, 'tasks.db')
|
| 41 |
|
| 42 |
def get_db_connection():
|
|
@@ -86,20 +86,13 @@ def update_task_final_status(task_id, status, error_message=None, output_filenam
|
|
| 86 |
conn.close()
|
| 87 |
|
| 88 |
def load_api_keys(prefix):
|
| 89 |
-
"""
|
| 90 |
-
prefix (जैसे 'Gemini_Key') से शुरू होने वाली सभी environment variables को लोड करता है।
|
| 91 |
-
यह 'Gemini_Key_1', 'Gemini_Key_2' आदि को अपने आप पहचान लेगा।
|
| 92 |
-
"""
|
| 93 |
try:
|
| 94 |
prefix_lower = prefix.lower()
|
| 95 |
-
# Environment variables को चेक करें (case-insensitive search)
|
| 96 |
keys = [v.strip() for k, v in os.environ.items() if k.lower().startswith(prefix_lower) and v.strip()]
|
| 97 |
-
|
| 98 |
if keys:
|
| 99 |
-
print(f"✅ API Key Check: '{prefix}' के लिए {len(keys)} कीज़
|
| 100 |
else:
|
| 101 |
print(f"❌ API Key Check: '{prefix}' के लिए कोई कीज़ नहीं मिलीं!")
|
| 102 |
-
|
| 103 |
return keys
|
| 104 |
except Exception as e:
|
| 105 |
print(f"🚨 एनवायरनमेंट वेरिएबल्स लोड करते समय त्रुटि: {e}")
|
|
@@ -211,7 +204,8 @@ class GeminiTeam:
|
|
| 211 |
|
| 212 |
def create_master_timeline(self, word_timestamps, enriched_scenes_with_paths):
|
| 213 |
full_script_text = " ".join([word['word'] for word in word_timestamps]); total_duration = word_timestamps[-1]['end'] if word_timestamps else 0
|
| 214 |
-
|
|
|
|
| 215 |
result = self._make_resilient_api_call(prompt, timeout=180)
|
| 216 |
json_str = result['candidates'][0]['content']['parts'][0]['text']
|
| 217 |
clean_str = json_str[json_str.find('['):json_str.rfind(']') + 1]; final_timeline = json.loads(clean_str)
|
|
@@ -225,7 +219,7 @@ class GeminiTeam:
|
|
| 225 |
print("-> Gemini ने सफलतापूर्वक स्क्रिप्ट जेनरेट कर दी है।"); return generated_script.strip()
|
| 226 |
|
| 227 |
# ==============================================================================
|
| 228 |
-
# 3. Enhanced Video Assembler (
|
| 229 |
# ==============================================================================
|
| 230 |
|
| 231 |
class VideoAssembler:
|
|
@@ -253,8 +247,8 @@ class VideoAssembler:
|
|
| 253 |
def assemble_video(self, log_callback):
|
| 254 |
if not self.timeline: return
|
| 255 |
|
| 256 |
-
# --- STAGE 1:
|
| 257 |
-
log_callback("-> Stage 1/3: क्लिप्स
|
| 258 |
prepared_clips = []
|
| 259 |
|
| 260 |
for i, item in enumerate(self.timeline):
|
|
@@ -278,14 +272,13 @@ class VideoAssembler:
|
|
| 278 |
|
| 279 |
output_clip_path = os.path.join(self.temp_dir, f"prepared_{i:03d}.mp4")
|
| 280 |
|
| 281 |
-
# 🔥
|
| 282 |
-
#
|
| 283 |
-
|
| 284 |
|
| 285 |
-
# High Quality settings for Hugging Face
|
| 286 |
command = [
|
| 287 |
'ffmpeg', '-y', '-ss', str(start_offset), '-i', input_clip_path, '-t', str(duration),
|
| 288 |
-
'-vf', f"scale={self.width
|
| 289 |
'-c:v', 'libx264', '-preset', 'medium', '-crf', '23', '-an', '-threads', '2',
|
| 290 |
output_clip_path
|
| 291 |
]
|
|
@@ -347,13 +340,11 @@ def run_ai_engine_worker(task_id, script_text, script_file_path, orientation, ma
|
|
| 347 |
|
| 348 |
try:
|
| 349 |
log("Step 0: API Keys की पुष्टि...", 2)
|
| 350 |
-
# कीज़ लोड करें
|
| 351 |
gemini_keys = load_api_keys("Gemini_Key")
|
| 352 |
pexels_keys = load_api_keys("Pexels_Key")
|
| 353 |
pixabay_keys = load_api_keys("Pixabay_Key")
|
| 354 |
groq_keys = load_api_keys("Groq_Key")
|
| 355 |
|
| 356 |
-
# अगर कोई की गायब है तो सटीक Error दें
|
| 357 |
missing = []
|
| 358 |
if not gemini_keys: missing.append("Gemini_Key")
|
| 359 |
if not pexels_keys: missing.append("Pexels_Key")
|
|
@@ -361,7 +352,7 @@ def run_ai_engine_worker(task_id, script_text, script_file_path, orientation, ma
|
|
| 361 |
if not groq_keys: missing.append("Groq_Key")
|
| 362 |
|
| 363 |
if missing:
|
| 364 |
-
raise Exception(f"API Key Error: ये कीज़ नहीं मिलीं: {', '.join(missing)}
|
| 365 |
|
| 366 |
gemini = GeminiTeam(api_keys=gemini_keys); log("-> सभी जरूरी API कीज मौजूद हैं।", 5)
|
| 367 |
|
|
@@ -455,11 +446,10 @@ def run_ai_engine_worker(task_id, script_text, script_file_path, orientation, ma
|
|
| 455 |
if last_clip_end < total_duration: clip['end'] = total_duration
|
| 456 |
final_gapless_timeline.append(clip)
|
| 457 |
|
| 458 |
-
log("Step 5: फाइनल वीडियो को रेंडर किया जा रहा है (
|
| 459 |
width, height = (1080, 1920) if orientation == 'vertical' else (1920, 1080)
|
| 460 |
output_filename = f"{task_id}_final_video.mp4"; output_path = os.path.join(OUTPUT_FOLDER, output_filename)
|
| 461 |
|
| 462 |
-
# Using the Enhanced VideoAssembler
|
| 463 |
assembler = VideoAssembler(final_gapless_timeline, narration_audio_path, output_path, width, height, mute_final_video, temp_dir)
|
| 464 |
assembler.assemble_video(log)
|
| 465 |
|
|
|
|
| 1 |
# ==============================================================================
|
| 2 |
+
# engine.py - [FINAL FIXED VERSION]
|
| 3 |
+
# FIX: Removed 'zoompan' which was pausing videos. Restored natural video motion.
|
| 4 |
# ==============================================================================
|
| 5 |
|
| 6 |
import os
|
|
|
|
| 28 |
# 1. Global Setup and Database Functions
|
| 29 |
# ==============================================================================
|
| 30 |
|
| 31 |
+
# प्रोजेक्ट की रूट डायरेक्टरी
|
| 32 |
APP_ROOT = '/code'
|
| 33 |
|
| 34 |
+
# सभी ज़रूरी फोल्डरों के लिए एब्सोल्यूट पाथ
|
| 35 |
DATA_FOLDER = os.path.join(APP_ROOT, 'data')
|
| 36 |
UPLOAD_FOLDER = os.path.join(APP_ROOT, 'uploads')
|
| 37 |
OUTPUT_FOLDER = os.path.join(APP_ROOT, 'outputs')
|
| 38 |
|
| 39 |
+
# डेटाबेस फाइल का पूरा एब्सोल्यूट पाथ
|
| 40 |
DATABASE_FILE = os.path.join(DATA_FOLDER, 'tasks.db')
|
| 41 |
|
| 42 |
def get_db_connection():
|
|
|
|
| 86 |
conn.close()
|
| 87 |
|
| 88 |
def load_api_keys(prefix):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
try:
|
| 90 |
prefix_lower = prefix.lower()
|
|
|
|
| 91 |
keys = [v.strip() for k, v in os.environ.items() if k.lower().startswith(prefix_lower) and v.strip()]
|
|
|
|
| 92 |
if keys:
|
| 93 |
+
print(f"✅ API Key Check: '{prefix}' के लिए {len(keys)} कीज़ मिलीं।")
|
| 94 |
else:
|
| 95 |
print(f"❌ API Key Check: '{prefix}' के लिए कोई कीज़ नहीं मिलीं!")
|
|
|
|
| 96 |
return keys
|
| 97 |
except Exception as e:
|
| 98 |
print(f"🚨 एनवायरनमेंट वेरिएबल्स लोड करते समय त्रुटि: {e}")
|
|
|
|
| 204 |
|
| 205 |
def create_master_timeline(self, word_timestamps, enriched_scenes_with_paths):
|
| 206 |
full_script_text = " ".join([word['word'] for word in word_timestamps]); total_duration = word_timestamps[-1]['end'] if word_timestamps else 0
|
| 207 |
+
# NOTE: Added instructions to avoid excessively long clips at the end
|
| 208 |
+
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:"""
|
| 209 |
result = self._make_resilient_api_call(prompt, timeout=180)
|
| 210 |
json_str = result['candidates'][0]['content']['parts'][0]['text']
|
| 211 |
clean_str = json_str[json_str.find('['):json_str.rfind(']') + 1]; final_timeline = json.loads(clean_str)
|
|
|
|
| 219 |
print("-> Gemini ने सफलतापूर्वक स्क्रिप्ट जेनरेट कर दी है।"); return generated_script.strip()
|
| 220 |
|
| 221 |
# ==============================================================================
|
| 222 |
+
# 3. Enhanced Video Assembler (FIXED: NO FREEZE)
|
| 223 |
# ==============================================================================
|
| 224 |
|
| 225 |
class VideoAssembler:
|
|
|
|
| 247 |
def assemble_video(self, log_callback):
|
| 248 |
if not self.timeline: return
|
| 249 |
|
| 250 |
+
# --- STAGE 1: Clip Preparation (Motion Restored) ---
|
| 251 |
+
log_callback("-> Stage 1/3: क्लिप्स तैयार की जा रही हैं (Normal Motion)...", 91)
|
| 252 |
prepared_clips = []
|
| 253 |
|
| 254 |
for i, item in enumerate(self.timeline):
|
|
|
|
| 272 |
|
| 273 |
output_clip_path = os.path.join(self.temp_dir, f"prepared_{i:03d}.mp4")
|
| 274 |
|
| 275 |
+
# 🔥 FIX: No 'zoompan'. Use scale and crop to keep video moving.
|
| 276 |
+
# scale='w=WIDTH:h=HEIGHT:force_original_aspect_ratio=increase' ensures cover
|
| 277 |
+
# crop='WIDTH:HEIGHT' centers the video
|
| 278 |
|
|
|
|
| 279 |
command = [
|
| 280 |
'ffmpeg', '-y', '-ss', str(start_offset), '-i', input_clip_path, '-t', str(duration),
|
| 281 |
+
'-vf', f"scale='w={self.width}:h={self.height}:force_original_aspect_ratio=increase',crop={self.width}:{self.height},setsar=1,fps=30",
|
| 282 |
'-c:v', 'libx264', '-preset', 'medium', '-crf', '23', '-an', '-threads', '2',
|
| 283 |
output_clip_path
|
| 284 |
]
|
|
|
|
| 340 |
|
| 341 |
try:
|
| 342 |
log("Step 0: API Keys की पुष्टि...", 2)
|
|
|
|
| 343 |
gemini_keys = load_api_keys("Gemini_Key")
|
| 344 |
pexels_keys = load_api_keys("Pexels_Key")
|
| 345 |
pixabay_keys = load_api_keys("Pixabay_Key")
|
| 346 |
groq_keys = load_api_keys("Groq_Key")
|
| 347 |
|
|
|
|
| 348 |
missing = []
|
| 349 |
if not gemini_keys: missing.append("Gemini_Key")
|
| 350 |
if not pexels_keys: missing.append("Pexels_Key")
|
|
|
|
| 352 |
if not groq_keys: missing.append("Groq_Key")
|
| 353 |
|
| 354 |
if missing:
|
| 355 |
+
raise Exception(f"API Key Error: ये कीज़ नहीं मिलीं: {', '.join(missing)}")
|
| 356 |
|
| 357 |
gemini = GeminiTeam(api_keys=gemini_keys); log("-> सभी जरूरी API कीज मौजूद हैं।", 5)
|
| 358 |
|
|
|
|
| 446 |
if last_clip_end < total_duration: clip['end'] = total_duration
|
| 447 |
final_gapless_timeline.append(clip)
|
| 448 |
|
| 449 |
+
log("Step 5: फाइनल वीडियो को रेंडर किया जा रहा है (Natural Motion, No Freeze)...", 90)
|
| 450 |
width, height = (1080, 1920) if orientation == 'vertical' else (1920, 1080)
|
| 451 |
output_filename = f"{task_id}_final_video.mp4"; output_path = os.path.join(OUTPUT_FOLDER, output_filename)
|
| 452 |
|
|
|
|
| 453 |
assembler = VideoAssembler(final_gapless_timeline, narration_audio_path, output_path, width, height, mute_final_video, temp_dir)
|
| 454 |
assembler.assemble_video(log)
|
| 455 |
|