Update app.py
Browse files
app.py
CHANGED
@@ -1,330 +1,171 @@
|
|
1 |
import os
|
2 |
-
import
|
|
|
|
|
3 |
import urllib.request
|
4 |
from PIL import Image
|
5 |
from gtts import gTTS
|
6 |
-
import cv2
|
7 |
import moviepy.editor as mp
|
8 |
-
import logging
|
9 |
-
import uuid
|
10 |
-
import time
|
11 |
import gradio as gr
|
12 |
-
|
13 |
-
import requests
|
14 |
-
from random import randint
|
15 |
-
# Configure logging
|
16 |
-
log_dir = os.getenv('LOG_DIRECTORY', './')
|
17 |
-
LOGGER_FILE_PATH = os.path.join(str(log_dir), 'utils.log')
|
18 |
|
|
|
19 |
logging.basicConfig(
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
24 |
)
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
if log_level_env in log_level_dict:
|
36 |
-
log_level = log_level_dict[log_level_env]
|
37 |
-
else:
|
38 |
-
log_level = log_level_dict['INFO']
|
39 |
-
LOGGER.setLevel(log_level)
|
40 |
-
|
41 |
-
|
42 |
-
class Text2Video:
|
43 |
-
"""A class to generate videos from text prompts."""
|
44 |
-
|
45 |
-
def __init__(self) -> None:
|
46 |
-
"""
|
47 |
-
Initialize the Text2Video class.
|
48 |
-
Args:
|
49 |
-
file_path (str): Path to the configuration file.
|
50 |
-
"""
|
51 |
-
# Replace Azure OpenAI with Hercai
|
52 |
-
self.hercai = Hercai("") # Replace "" with your Hercai API key if you have one
|
53 |
-
self.prodia_model = "stable-diffusion-xl"
|
54 |
-
self.pollinations_model = None
|
55 |
-
|
56 |
-
def get_image(self, img_prompt: str) -> str:
|
57 |
-
"""
|
58 |
-
Generate an image based on the provided text prompt using Hercai's draw_image method.
|
59 |
-
Args:
|
60 |
-
img_prompt (str): Text prompt for generating the image.
|
61 |
-
Returns:
|
62 |
-
str: URL of the generated image.
|
63 |
-
"""
|
64 |
try:
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
except Exception as e:
|
71 |
-
|
72 |
-
|
73 |
-
return ""
|
74 |
|
75 |
-
def
|
76 |
-
"""
|
77 |
-
Download an image from a URL.
|
78 |
-
Args:
|
79 |
-
image_url (str): URL of the image to download.
|
80 |
-
image_path (str): Path to save the downloaded image.
|
81 |
-
Returns:
|
82 |
-
str: Path of the downloaded image.
|
83 |
-
"""
|
84 |
try:
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
except Exception as e:
|
90 |
-
|
91 |
-
|
92 |
-
return "" # Return an empty string if an error occurs
|
93 |
|
94 |
-
def
|
95 |
-
"""
|
96 |
-
Convert text to speech and save it as an audio file.
|
97 |
-
Args:
|
98 |
-
img_prompt (str): Text to convert to speech.
|
99 |
-
audio_path (str): Path to save the audio file.
|
100 |
-
Returns:
|
101 |
-
str: Path of the saved audio file.
|
102 |
-
"""
|
103 |
try:
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
# Save the audio file at the specified path
|
110 |
-
myobj.save(audio_path)
|
111 |
-
|
112 |
-
# Return the path of the saved audio file if successful
|
113 |
-
return audio_path
|
114 |
except Exception as e:
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
""
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
# Generate a unique identifier for this file
|
133 |
-
unique_id = uuid.uuid4().hex
|
134 |
-
|
135 |
-
# Construct the image path using the unique identifier
|
136 |
-
image_path = f"{img_prompt[:9]}_{unique_id}.png"
|
137 |
-
|
138 |
-
# Generate image URL based on the prompt
|
139 |
-
img_url = self.get_image(img_prompt)
|
140 |
-
|
141 |
-
# Download and save the image
|
142 |
-
image = self.download_img_from_url(img_url, image_path)
|
143 |
-
|
144 |
-
# Add the image path to the list
|
145 |
-
img_list.append(image)
|
146 |
-
|
147 |
-
# Construct the audio path using the unique identifier
|
148 |
-
audio_path = f"{img_prompt[:9]}_{unique_id}.mp3"
|
149 |
-
|
150 |
-
# Convert text to audio and save it
|
151 |
-
audio = self.text_to_audio(img_prompt, audio_path)
|
152 |
-
|
153 |
-
# Add the audio path to the list
|
154 |
-
audio_paths.append(audio)
|
155 |
-
|
156 |
-
except Exception as e:
|
157 |
-
LOGGER.error(f"Error processing prompt: {img_prompt}, {e}")
|
158 |
-
|
159 |
-
# Return lists of image paths and audio paths as a tuple
|
160 |
-
return img_list, audio_paths
|
161 |
-
|
162 |
-
def create_video_from_images_and_audio(self, image_files: list, audio_files: list, output_path: str) -> None:
|
163 |
-
"""
|
164 |
-
Create a video from images and corresponding audio files.
|
165 |
-
Args:
|
166 |
-
image_files (list): List of image files.
|
167 |
-
audio_files (list): List of audio files.
|
168 |
-
output_path (str): Path to save the output video file.
|
169 |
-
"""
|
170 |
try:
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
# Set audio for the video clip
|
191 |
-
video_clip = video_clip.set_audio(audio_clip)
|
192 |
-
|
193 |
-
# Append the video clip to the list
|
194 |
-
video_clips.append(video_clip)
|
195 |
-
|
196 |
-
# Concatenate all video clips into a single clip
|
197 |
-
final_clip = mp.concatenate_videoclips(video_clips)
|
198 |
-
|
199 |
-
# Write the final video to the output path
|
200 |
-
final_clip.write_videofile(output_path, codec='libx264', fps=24)
|
201 |
-
print("Video created successfully.")
|
202 |
-
|
203 |
except Exception as e:
|
204 |
-
|
205 |
-
|
206 |
|
207 |
-
def
|
208 |
-
"""
|
209 |
-
Generate a video from a list of text prompts.
|
210 |
-
Args:
|
211 |
-
list_prompts (list): List of text prompts.
|
212 |
-
"""
|
213 |
try:
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
self.
|
224 |
-
|
225 |
-
return output_path
|
226 |
except Exception as e:
|
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 |
-
headers = {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"}
|
260 |
-
|
261 |
-
resp = s.get(
|
262 |
-
"https://api.prodia.com/generate",
|
263 |
-
params={
|
264 |
-
"new": "true", "prompt": prompt, "model": model,
|
265 |
-
"negative_prompt": "verybadimagenegative_v1.3",
|
266 |
-
"steps": "20", "cfg": "7", "seed": randint(1, 10000),
|
267 |
-
"sample": "DPM++ 2M Karras", "aspect_ratio": "square"
|
268 |
-
},
|
269 |
-
headers=headers
|
270 |
)
|
271 |
-
|
272 |
-
|
273 |
-
while True:
|
274 |
-
time.sleep(5)
|
275 |
-
status = s.get(f"https://api.prodia.com/job/{job_id}", headers=headers).json()
|
276 |
-
if status["status"] == "succeeded":
|
277 |
-
img_data = s.get(f"https://images.prodia.xyz/{job_id}.png?download=1", headers=headers).content
|
278 |
-
with open(output_file, 'wb') as f:
|
279 |
-
f.write(img_data)
|
280 |
-
return output_file
|
281 |
-
return None
|
282 |
-
|
283 |
-
def pollinations_generate(self, prompt, output_file="pollinations_output.png"):
|
284 |
-
response = requests.get(f"https://image.pollinations.ai/prompt/{prompt}{randint(1, 10000)}")
|
285 |
-
if response.status_code == 200:
|
286 |
-
with open(output_file, 'wb') as f:
|
287 |
-
f.write(response.content)
|
288 |
-
return output_file
|
289 |
-
return None
|
290 |
-
|
291 |
-
# --- Hercai Class ---
|
292 |
-
class Hercai:
|
293 |
-
def __init__(self, api_key=None):
|
294 |
-
self.api_key = api_key
|
295 |
-
|
296 |
-
def question(self, model="v3", content="", personality=None):
|
297 |
-
url = f"https://hercai.onrender.com/v3/hercai?question={content}&model={model}"
|
298 |
-
if personality:
|
299 |
-
url += f"&personality={personality}"
|
300 |
-
if self.api_key:
|
301 |
-
url += f"&key={self.api_key}"
|
302 |
-
response = requests.get(url)
|
303 |
-
return response.json()
|
304 |
-
|
305 |
-
def draw_image(self, model="v3", prompt="", negative_prompt=""):
|
306 |
-
url = f"https://hercai.onrender.com/v3/text2image?prompt={prompt}&model={model}&negative_prompt={negative_prompt}"
|
307 |
-
if self.api_key:
|
308 |
-
url += f"&key={self.api_key}"
|
309 |
-
response = requests.get(url)
|
310 |
-
return response.json()
|
311 |
-
|
312 |
-
def beta_question(self, model="v3", content="", personality=None):
|
313 |
-
url = f"https://hercai.onrender.com/beta/hercai?question={content}&model={model}"
|
314 |
-
if personality:
|
315 |
-
url += f"&personality={personality}"
|
316 |
-
if self.api_key:
|
317 |
-
url += f"&key={self.api_key}"
|
318 |
-
response = requests.get(url)
|
319 |
-
return response.json()
|
320 |
-
|
321 |
-
def beta_draw_image(self, model="v3", prompt="", negative_prompt=""):
|
322 |
-
url = f"https://hercai.onrender.com/beta/text2image?prompt={prompt}&model={model}&negative_prompt={negative_prompt}"
|
323 |
-
if self.api_key:
|
324 |
-
url += f"&key={self.api_key}"
|
325 |
-
response = requests.get(url)
|
326 |
-
return response.json()
|
327 |
|
328 |
if __name__ == "__main__":
|
329 |
-
|
330 |
-
|
|
|
1 |
import os
|
2 |
+
import logging
|
3 |
+
import uuid
|
4 |
+
from typing import List, Tuple
|
5 |
import urllib.request
|
6 |
from PIL import Image
|
7 |
from gtts import gTTS
|
|
|
8 |
import moviepy.editor as mp
|
|
|
|
|
|
|
9 |
import gradio as gr
|
10 |
+
from hercai import Hercai
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
+
# Configure logging
|
13 |
logging.basicConfig(
|
14 |
+
level=logging.INFO,
|
15 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
16 |
+
handlers=[
|
17 |
+
logging.FileHandler('comic_generator.log'),
|
18 |
+
logging.StreamHandler()
|
19 |
+
]
|
20 |
)
|
21 |
+
logger = logging.getLogger(__name__)
|
22 |
+
|
23 |
+
class ComicVideoGenerator:
|
24 |
+
def __init__(self):
|
25 |
+
"""Initialize comic video generator with Hercai API"""
|
26 |
+
self.api = Hercai()
|
27 |
+
self.image_size = (1024, 1024)
|
28 |
+
|
29 |
+
def generate_comic_image(self, prompt: str) -> str:
|
30 |
+
"""Generate comic-style image from text prompt"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
try:
|
32 |
+
enhanced_prompt = (
|
33 |
+
f"{prompt}, comic book style, vibrant colors, "
|
34 |
+
"clear speech bubbles, dramatic lighting, "
|
35 |
+
"detailed backgrounds, professional illustration"
|
36 |
+
)
|
37 |
+
|
38 |
+
result = self.api.draw_image(
|
39 |
+
model="simurg",
|
40 |
+
prompt=enhanced_prompt,
|
41 |
+
negative_prompt="blurry, low quality, dark"
|
42 |
+
)
|
43 |
+
logger.info(f"Generated image for prompt: {prompt[:30]}...")
|
44 |
+
return result["url"]
|
45 |
+
|
46 |
except Exception as e:
|
47 |
+
logger.error(f"Image generation failed: {e}")
|
48 |
+
raise
|
|
|
49 |
|
50 |
+
def process_image(self, url: str, save_path: str) -> str:
|
51 |
+
"""Download and process image to correct size"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
try:
|
53 |
+
urllib.request.urlretrieve(url, save_path)
|
54 |
+
|
55 |
+
with Image.open(save_path) as img:
|
56 |
+
img.thumbnail(self.image_size)
|
57 |
+
new_img = Image.new('RGB', self.image_size, 'white')
|
58 |
+
offset = tuple(map(lambda x, y: (x - y) // 2,
|
59 |
+
self.image_size, img.size))
|
60 |
+
new_img.paste(img, offset)
|
61 |
+
new_img.save(save_path, quality=95)
|
62 |
+
|
63 |
+
logger.info(f"Processed and saved image: {save_path}")
|
64 |
+
return save_path
|
65 |
+
|
66 |
except Exception as e:
|
67 |
+
logger.error(f"Image processing failed: {e}")
|
68 |
+
raise
|
|
|
69 |
|
70 |
+
def create_audio(self, text: str, save_path: str) -> str:
|
71 |
+
"""Generate audio narration from text"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
try:
|
73 |
+
tts = gTTS(text=text, lang='en')
|
74 |
+
tts.save(save_path)
|
75 |
+
logger.info(f"Created audio: {save_path}")
|
76 |
+
return save_path
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
except Exception as e:
|
78 |
+
logger.error(f"Audio generation failed: {e}")
|
79 |
+
raise
|
80 |
+
|
81 |
+
def process_scene(self, prompt: str, scene_id: str) -> Tuple[str, str]:
|
82 |
+
"""Process single scene (image + audio)"""
|
83 |
+
image_path = f"scene_{scene_id}.png"
|
84 |
+
audio_path = f"audio_{scene_id}.mp3"
|
85 |
+
|
86 |
+
image_url = self.generate_comic_image(prompt)
|
87 |
+
image_file = self.process_image(image_url, image_path)
|
88 |
+
audio_file = self.create_audio(prompt, audio_path)
|
89 |
+
|
90 |
+
return image_file, audio_file
|
91 |
+
|
92 |
+
def create_video(self, images: List[str], audios: List[str],
|
93 |
+
output_path: str) -> str:
|
94 |
+
"""Create final video from images and audio"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
try:
|
96 |
+
clips = []
|
97 |
+
for img, audio in zip(images, audios):
|
98 |
+
audio_clip = mp.AudioFileClip(audio)
|
99 |
+
video_clip = (mp.ImageClip(img)
|
100 |
+
.set_duration(audio_clip.duration)
|
101 |
+
.set_audio(audio_clip))
|
102 |
+
clips.append(video_clip)
|
103 |
+
|
104 |
+
final_clip = mp.concatenate_videoclips(clips)
|
105 |
+
final_clip.write_videofile(
|
106 |
+
output_path,
|
107 |
+
fps=24,
|
108 |
+
codec='libx264',
|
109 |
+
audio_codec='aac'
|
110 |
+
)
|
111 |
+
logger.info(f"Created video: {output_path}")
|
112 |
+
return output_path
|
113 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
except Exception as e:
|
115 |
+
logger.error(f"Video creation failed: {e}")
|
116 |
+
raise
|
117 |
|
118 |
+
def generate(self, text: str) -> str:
|
119 |
+
"""Main video generation pipeline"""
|
|
|
|
|
|
|
|
|
120 |
try:
|
121 |
+
scenes = [s.strip() for s in text.split(",,") if s.strip()]
|
122 |
+
output_path = f"comic_{uuid.uuid4().hex[:8]}.mp4"
|
123 |
+
|
124 |
+
images, audios = [], []
|
125 |
+
for i, scene in enumerate(scenes):
|
126 |
+
img, audio = self.process_scene(scene, f"{i}")
|
127 |
+
images.append(img)
|
128 |
+
audios.append(audio)
|
129 |
+
|
130 |
+
return self.create_video(images, audios, output_path)
|
131 |
+
|
|
|
132 |
except Exception as e:
|
133 |
+
logger.error(f"Generation pipeline failed: {e}")
|
134 |
+
raise
|
135 |
+
|
136 |
+
def create_interface():
|
137 |
+
"""Create Gradio interface"""
|
138 |
+
generator = ComicVideoGenerator()
|
139 |
+
|
140 |
+
examples = [
|
141 |
+
"A magical forest at sunset.,, A brave knight finds a glowing crystal.,, The crystal transforms into a dragon.",
|
142 |
+
"A busy city street.,, A mysterious package appears.,, The package opens to reveal a portal."
|
143 |
+
]
|
144 |
+
|
145 |
+
with gr.Blocks(theme='default') as demo:
|
146 |
+
gr.Markdown("# Comic Video Generator")
|
147 |
+
|
148 |
+
with gr.Row():
|
149 |
+
text_input = gr.Textbox(
|
150 |
+
label="Story Text",
|
151 |
+
placeholder="Enter story scenes separated by ',,'",
|
152 |
+
lines=3
|
153 |
+
)
|
154 |
+
|
155 |
+
with gr.Row():
|
156 |
+
generate_btn = gr.Button("Generate Comic")
|
157 |
+
video_output = gr.Video(label="Generated Comic")
|
158 |
+
|
159 |
+
gr.Examples(examples, text_input)
|
160 |
+
|
161 |
+
generate_btn.click(
|
162 |
+
fn=generator.generate,
|
163 |
+
inputs=text_input,
|
164 |
+
outputs=video_output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
)
|
166 |
+
|
167 |
+
return demo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
168 |
|
169 |
if __name__ == "__main__":
|
170 |
+
interface = create_interface()
|
171 |
+
interface.launch(debug=True)
|