Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,27 +1,29 @@
|
|
1 |
import gradio as gr
|
2 |
import os
|
3 |
import google.generativeai as genai
|
4 |
-
|
|
|
5 |
from tavily import TavilyClient
|
6 |
-
import requests
|
7 |
import subprocess
|
8 |
import json
|
9 |
import time
|
10 |
import random
|
11 |
|
12 |
# --- 1. CONFIGURE API KEYS FROM HUGGING FACE SECRETS ---
|
13 |
-
# Ensure you have set these in your Space's settings -> secrets
|
14 |
try:
|
15 |
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
|
16 |
-
elevenlabs.set_api_key(os.environ["ELEVENLABS_API_KEY"])
|
17 |
tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
|
18 |
RUNWAY_API_KEY = os.environ["RUNWAY_API_KEY"]
|
|
|
|
|
|
|
|
|
19 |
except KeyError as e:
|
20 |
raise ValueError(f"API Key Error: Please set the {e} secret in your Hugging Face Space settings.")
|
21 |
|
22 |
# --- 2. DEFINE API ENDPOINTS AND HEADERS ---
|
23 |
-
|
24 |
-
RUNWAY_API_URL = "https://api.runwayml.com/v1/jobs"
|
25 |
RUNWAY_HEADERS = {
|
26 |
"Authorization": f"Bearer {RUNWAY_API_KEY}",
|
27 |
"Content-Type": "application/json"
|
@@ -29,12 +31,6 @@ RUNWAY_HEADERS = {
|
|
29 |
|
30 |
# --- 3. THE CORE VIDEO GENERATION FUNCTION ---
|
31 |
def generate_video_from_topic(topic_prompt, progress=gr.Progress(track_tqdm=True)):
|
32 |
-
"""
|
33 |
-
Main function to orchestrate the video generation pipeline.
|
34 |
-
It takes a topic and returns the path to the final generated video.
|
35 |
-
"""
|
36 |
-
|
37 |
-
# Use a unique ID for this job to prevent file collisions
|
38 |
job_id = f"{int(time.time())}_{random.randint(1000, 9999)}"
|
39 |
print(f"--- Starting New Job: {job_id} for topic: '{topic_prompt}' ---")
|
40 |
|
@@ -64,22 +60,10 @@ def generate_video_from_topic(topic_prompt, progress=gr.Progress(track_tqdm=True
|
|
64 |
Your output MUST be a valid JSON object with two keys:
|
65 |
1. "narration_script": A string containing the full voiceover narration. Make it engaging and concise.
|
66 |
2. "scene_prompts": A list of exactly 4 strings. Each string must be a highly detailed, visually rich, and cinematic prompt for a text-to-video AI like Runway Gen-2. Describe camera angles, lighting, and mood.
|
67 |
-
|
68 |
-
Example JSON format:
|
69 |
-
{{
|
70 |
-
"narration_script": "Did you know the ocean's depths hold more history than all the world's museums combined? Let's dive in...",
|
71 |
-
"scene_prompts": [
|
72 |
-
"An ultra-realistic, cinematic shot of a massive blue whale gliding through deep, sun-dappled ocean water, camera tracking smoothly alongside it.",
|
73 |
-
"A dramatic, slow-motion close-up of an ancient shipwreck on the seabed, covered in coral, with schools of small fish swimming through its broken hull.",
|
74 |
-
"A bioluminescent jellyfish pulsing with ethereal light in the pitch-black abyss, shot with a macro lens.",
|
75 |
-
"A wide, epic shot of a volcanic vent on the ocean floor erupting with dark smoke, viewed from a safe distance, creating a sense of immense power."
|
76 |
-
]
|
77 |
-
}}
|
78 |
"""
|
79 |
response = gemini_model.generate_content(prompt)
|
80 |
|
81 |
try:
|
82 |
-
# Clean the response text before parsing
|
83 |
cleaned_text = response.text.strip().replace("```json", "").replace("```", "")
|
84 |
script_data = json.loads(cleaned_text)
|
85 |
narration = script_data['narration_script']
|
@@ -92,30 +76,32 @@ def generate_video_from_topic(topic_prompt, progress=gr.Progress(track_tqdm=True
|
|
92 |
progress(0.3, desc="🎙️ Recording voiceover with ElevenLabs...")
|
93 |
audio_path = f"audio_{job_id}.mp3"
|
94 |
intermediate_files.append(audio_path)
|
95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
with open(audio_path, "wb") as f:
|
97 |
f.write(audio_bytes)
|
98 |
print(f"Audio file saved: {audio_path}")
|
99 |
|
100 |
-
# STEP 4: VISUALS (Runway)
|
101 |
video_clip_paths = []
|
102 |
for i, scene_prompt in enumerate(scene_prompts):
|
103 |
progress(0.4 + (i * 0.12), desc=f"🎬 Generating video scene {i+1}/{len(scene_prompts)}...")
|
104 |
-
|
105 |
-
# A. Start the generation job
|
106 |
runway_payload = {"text_prompt": scene_prompt}
|
107 |
post_response = requests.post(RUNWAY_API_URL, headers=RUNWAY_HEADERS, json=runway_payload)
|
108 |
if post_response.status_code != 200:
|
109 |
raise gr.Error(f"Runway API Error (start job): {post_response.status_code} - {post_response.text}")
|
110 |
|
111 |
-
|
112 |
-
task_id = job_details.get("uuid")
|
113 |
if not task_id:
|
114 |
-
raise gr.Error(f"Runway API did not return a task UUID. Response: {
|
115 |
|
116 |
-
# B. Poll for job completion
|
117 |
video_url = None
|
118 |
-
for _ in range(60):
|
119 |
get_response = requests.get(f"{RUNWAY_API_URL}/{task_id}", headers=RUNWAY_HEADERS)
|
120 |
status_details = get_response.json()
|
121 |
status = status_details.get("status")
|
@@ -130,9 +116,8 @@ def generate_video_from_topic(topic_prompt, progress=gr.Progress(track_tqdm=True
|
|
130 |
time.sleep(10)
|
131 |
|
132 |
if not video_url:
|
133 |
-
raise gr.Error(f"Runway job timed out
|
134 |
|
135 |
-
# C. Download the generated video
|
136 |
clip_path = f"scene_{i+1}_{job_id}.mp4"
|
137 |
intermediate_files.append(clip_path)
|
138 |
video_clip_paths.append(clip_path)
|
@@ -145,22 +130,17 @@ def generate_video_from_topic(topic_prompt, progress=gr.Progress(track_tqdm=True
|
|
145 |
|
146 |
# STEP 5: STITCHING (FFmpeg)
|
147 |
progress(0.9, desc="✂️ Assembling final video with FFmpeg...")
|
148 |
-
|
149 |
-
# Create a file list for ffmpeg
|
150 |
file_list_path = f"file_list_{job_id}.txt"
|
151 |
intermediate_files.append(file_list_path)
|
152 |
with open(file_list_path, "w") as f:
|
153 |
for clip in video_clip_paths:
|
154 |
f.write(f"file '{clip}'\n")
|
155 |
|
156 |
-
# Concatenate video clips
|
157 |
combined_video_path = f"combined_video_{job_id}.mp4"
|
158 |
intermediate_files.append(combined_video_path)
|
159 |
subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', file_list_path, '-c', 'copy', combined_video_path, '-y'], check=True)
|
160 |
|
161 |
-
# Add audio to the combined video
|
162 |
final_video_path = f"final_video_{job_id}.mp4"
|
163 |
-
# We don't add this to intermediate files because we want to keep it
|
164 |
subprocess.run([
|
165 |
'ffmpeg', '-i', combined_video_path, '-i', audio_path, '-c:v', 'copy',
|
166 |
'-c:a', 'aac', '-shortest', final_video_path, '-y'
|
@@ -171,20 +151,17 @@ def generate_video_from_topic(topic_prompt, progress=gr.Progress(track_tqdm=True
|
|
171 |
return final_video_path
|
172 |
|
173 |
except Exception as e:
|
174 |
-
# If anything goes wrong, raise a Gradio error to display it in the UI
|
175 |
print(f"--- JOB {job_id} FAILED --- \nError: {e}")
|
176 |
raise gr.Error(f"An error occurred: {e}")
|
177 |
|
178 |
finally:
|
179 |
# STEP 6: CLEANUP
|
180 |
-
# Clean up all the temporary files we created
|
181 |
print("Cleaning up intermediate files...")
|
182 |
for file_path in intermediate_files:
|
183 |
if os.path.exists(file_path):
|
184 |
os.remove(file_path)
|
185 |
print(f"Removed: {file_path}")
|
186 |
|
187 |
-
|
188 |
# --- 4. CREATE AND LAUNCH THE GRADIO INTERFACE ---
|
189 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
190 |
gr.Markdown(
|
|
|
1 |
import gradio as gr
|
2 |
import os
|
3 |
import google.generativeai as genai
|
4 |
+
# --- CHANGE 1: Import the new ElevenLabs client ---
|
5 |
+
from elevenlabs.client import ElevenLabs
|
6 |
from tavily import TavilyClient
|
7 |
+
import requests
|
8 |
import subprocess
|
9 |
import json
|
10 |
import time
|
11 |
import random
|
12 |
|
13 |
# --- 1. CONFIGURE API KEYS FROM HUGGING FACE SECRETS ---
|
|
|
14 |
try:
|
15 |
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
|
|
|
16 |
tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
|
17 |
RUNWAY_API_KEY = os.environ["RUNWAY_API_KEY"]
|
18 |
+
|
19 |
+
# --- CHANGE 2: Create an instance of the ElevenLabs client ---
|
20 |
+
elevenlabs_client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
|
21 |
+
|
22 |
except KeyError as e:
|
23 |
raise ValueError(f"API Key Error: Please set the {e} secret in your Hugging Face Space settings.")
|
24 |
|
25 |
# --- 2. DEFINE API ENDPOINTS AND HEADERS ---
|
26 |
+
RUNWAY_API_URL = "https://api.runwayml.com/v1/jobs"
|
|
|
27 |
RUNWAY_HEADERS = {
|
28 |
"Authorization": f"Bearer {RUNWAY_API_KEY}",
|
29 |
"Content-Type": "application/json"
|
|
|
31 |
|
32 |
# --- 3. THE CORE VIDEO GENERATION FUNCTION ---
|
33 |
def generate_video_from_topic(topic_prompt, progress=gr.Progress(track_tqdm=True)):
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
job_id = f"{int(time.time())}_{random.randint(1000, 9999)}"
|
35 |
print(f"--- Starting New Job: {job_id} for topic: '{topic_prompt}' ---")
|
36 |
|
|
|
60 |
Your output MUST be a valid JSON object with two keys:
|
61 |
1. "narration_script": A string containing the full voiceover narration. Make it engaging and concise.
|
62 |
2. "scene_prompts": A list of exactly 4 strings. Each string must be a highly detailed, visually rich, and cinematic prompt for a text-to-video AI like Runway Gen-2. Describe camera angles, lighting, and mood.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
"""
|
64 |
response = gemini_model.generate_content(prompt)
|
65 |
|
66 |
try:
|
|
|
67 |
cleaned_text = response.text.strip().replace("```json", "").replace("```", "")
|
68 |
script_data = json.loads(cleaned_text)
|
69 |
narration = script_data['narration_script']
|
|
|
76 |
progress(0.3, desc="🎙️ Recording voiceover with ElevenLabs...")
|
77 |
audio_path = f"audio_{job_id}.mp3"
|
78 |
intermediate_files.append(audio_path)
|
79 |
+
|
80 |
+
# --- CHANGE 3: Use the client instance to generate audio ---
|
81 |
+
audio_bytes = elevenlabs_client.generate(
|
82 |
+
text=narration,
|
83 |
+
voice="Adam",
|
84 |
+
model="eleven_multilingual_v2"
|
85 |
+
)
|
86 |
with open(audio_path, "wb") as f:
|
87 |
f.write(audio_bytes)
|
88 |
print(f"Audio file saved: {audio_path}")
|
89 |
|
90 |
+
# STEP 4: VISUALS (Runway)
|
91 |
video_clip_paths = []
|
92 |
for i, scene_prompt in enumerate(scene_prompts):
|
93 |
progress(0.4 + (i * 0.12), desc=f"🎬 Generating video scene {i+1}/{len(scene_prompts)}...")
|
|
|
|
|
94 |
runway_payload = {"text_prompt": scene_prompt}
|
95 |
post_response = requests.post(RUNWAY_API_URL, headers=RUNWAY_HEADERS, json=runway_payload)
|
96 |
if post_response.status_code != 200:
|
97 |
raise gr.Error(f"Runway API Error (start job): {post_response.status_code} - {post_response.text}")
|
98 |
|
99 |
+
task_id = post_response.json().get("uuid")
|
|
|
100 |
if not task_id:
|
101 |
+
raise gr.Error(f"Runway API did not return a task UUID. Response: {post_response.json()}")
|
102 |
|
|
|
103 |
video_url = None
|
104 |
+
for _ in range(60):
|
105 |
get_response = requests.get(f"{RUNWAY_API_URL}/{task_id}", headers=RUNWAY_HEADERS)
|
106 |
status_details = get_response.json()
|
107 |
status = status_details.get("status")
|
|
|
116 |
time.sleep(10)
|
117 |
|
118 |
if not video_url:
|
119 |
+
raise gr.Error(f"Runway job timed out for scene {i+1}.")
|
120 |
|
|
|
121 |
clip_path = f"scene_{i+1}_{job_id}.mp4"
|
122 |
intermediate_files.append(clip_path)
|
123 |
video_clip_paths.append(clip_path)
|
|
|
130 |
|
131 |
# STEP 5: STITCHING (FFmpeg)
|
132 |
progress(0.9, desc="✂️ Assembling final video with FFmpeg...")
|
|
|
|
|
133 |
file_list_path = f"file_list_{job_id}.txt"
|
134 |
intermediate_files.append(file_list_path)
|
135 |
with open(file_list_path, "w") as f:
|
136 |
for clip in video_clip_paths:
|
137 |
f.write(f"file '{clip}'\n")
|
138 |
|
|
|
139 |
combined_video_path = f"combined_video_{job_id}.mp4"
|
140 |
intermediate_files.append(combined_video_path)
|
141 |
subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', file_list_path, '-c', 'copy', combined_video_path, '-y'], check=True)
|
142 |
|
|
|
143 |
final_video_path = f"final_video_{job_id}.mp4"
|
|
|
144 |
subprocess.run([
|
145 |
'ffmpeg', '-i', combined_video_path, '-i', audio_path, '-c:v', 'copy',
|
146 |
'-c:a', 'aac', '-shortest', final_video_path, '-y'
|
|
|
151 |
return final_video_path
|
152 |
|
153 |
except Exception as e:
|
|
|
154 |
print(f"--- JOB {job_id} FAILED --- \nError: {e}")
|
155 |
raise gr.Error(f"An error occurred: {e}")
|
156 |
|
157 |
finally:
|
158 |
# STEP 6: CLEANUP
|
|
|
159 |
print("Cleaning up intermediate files...")
|
160 |
for file_path in intermediate_files:
|
161 |
if os.path.exists(file_path):
|
162 |
os.remove(file_path)
|
163 |
print(f"Removed: {file_path}")
|
164 |
|
|
|
165 |
# --- 4. CREATE AND LAUNCH THE GRADIO INTERFACE ---
|
166 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
167 |
gr.Markdown(
|