Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,7 @@ from transformers import pipeline, AutoModelForCausalLM, AutoTokenizer
|
|
| 6 |
from PIL import Image, ImageDraw, ImageFont
|
| 7 |
import tempfile
|
| 8 |
import os
|
| 9 |
-
from moviepy.editor import
|
| 10 |
import numpy as np
|
| 11 |
from gtts import gTTS
|
| 12 |
import textwrap
|
|
@@ -457,51 +457,17 @@ class ImageScraper:
|
|
| 457 |
return False
|
| 458 |
|
| 459 |
def generate_fallback_audio(self, script: str) -> AudioFileClip:
|
| 460 |
-
"""Generate fallback audio using gTTS
|
| 461 |
try:
|
| 462 |
audio_path = self.temp_dir / "voice.mp3"
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
max_chars = 5000
|
| 466 |
-
text_chunks = [script[i:i+max_chars] for i in range(0, len(script), max_chars)]
|
| 467 |
-
|
| 468 |
-
# Create temporary files for each chunk
|
| 469 |
-
temp_files = []
|
| 470 |
-
for i, chunk in enumerate(text_chunks):
|
| 471 |
-
temp_path = self.temp_dir / f"voice_chunk_{i}.mp3"
|
| 472 |
-
tts = gTTS(text=chunk, lang='en', slow=False)
|
| 473 |
-
tts.save(str(temp_path))
|
| 474 |
-
temp_files.append(temp_path)
|
| 475 |
-
|
| 476 |
-
# Concatenate audio files if there are multiple chunks
|
| 477 |
-
if len(temp_files) > 1:
|
| 478 |
-
clips = [AudioFileClip(str(f)) for f in temp_files]
|
| 479 |
-
final_clip = concatenate_audioclips(clips)
|
| 480 |
-
final_clip.write_audiofile(str(audio_path), codec='mp3')
|
| 481 |
-
final_clip.close()
|
| 482 |
-
for clip in clips:
|
| 483 |
-
clip.close()
|
| 484 |
-
else:
|
| 485 |
-
# Just rename the single chunk file
|
| 486 |
-
os.rename(temp_files[0], audio_path)
|
| 487 |
-
|
| 488 |
-
# Clean up temporary files
|
| 489 |
-
for temp_file in temp_files:
|
| 490 |
-
if temp_file.exists():
|
| 491 |
-
temp_file.unlink()
|
| 492 |
-
|
| 493 |
-
# Verify the audio file
|
| 494 |
-
if not audio_path.exists() or audio_path.stat().st_size == 0:
|
| 495 |
-
raise Exception("Audio file generation failed")
|
| 496 |
-
|
| 497 |
return AudioFileClip(str(audio_path))
|
| 498 |
-
|
| 499 |
except Exception as e:
|
| 500 |
-
|
| 501 |
-
# Create
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
def scrape_pexels(self, query: str) -> List[str]:
|
| 506 |
urls = []
|
| 507 |
try:
|
|
@@ -552,50 +518,16 @@ class EnhancedVideoGenerator:
|
|
| 552 |
|
| 553 |
|
| 554 |
def generate_fallback_audio(self, script: str) -> AudioFileClip:
|
| 555 |
-
"""Generate fallback audio using gTTS
|
| 556 |
try:
|
| 557 |
audio_path = self.temp_dir / "voice.mp3"
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
max_chars = 5000
|
| 561 |
-
text_chunks = [script[i:i+max_chars] for i in range(0, len(script), max_chars)]
|
| 562 |
-
|
| 563 |
-
# Create temporary files for each chunk
|
| 564 |
-
temp_files = []
|
| 565 |
-
for i, chunk in enumerate(text_chunks):
|
| 566 |
-
temp_path = self.temp_dir / f"voice_chunk_{i}.mp3"
|
| 567 |
-
tts = gTTS(text=chunk, lang='en', slow=False)
|
| 568 |
-
tts.save(str(temp_path))
|
| 569 |
-
temp_files.append(temp_path)
|
| 570 |
-
|
| 571 |
-
# Concatenate audio files if there are multiple chunks
|
| 572 |
-
if len(temp_files) > 1:
|
| 573 |
-
clips = [AudioFileClip(str(f)) for f in temp_files]
|
| 574 |
-
final_clip = concatenate_audioclips(clips)
|
| 575 |
-
final_clip.write_audiofile(str(audio_path), codec='mp3')
|
| 576 |
-
final_clip.close()
|
| 577 |
-
for clip in clips:
|
| 578 |
-
clip.close()
|
| 579 |
-
else:
|
| 580 |
-
# Just rename the single chunk file
|
| 581 |
-
os.rename(temp_files[0], audio_path)
|
| 582 |
-
|
| 583 |
-
# Clean up temporary files
|
| 584 |
-
for temp_file in temp_files:
|
| 585 |
-
if temp_file.exists():
|
| 586 |
-
temp_file.unlink()
|
| 587 |
-
|
| 588 |
-
# Verify the audio file
|
| 589 |
-
if not audio_path.exists() or audio_path.stat().st_size == 0:
|
| 590 |
-
raise Exception("Audio file generation failed")
|
| 591 |
-
|
| 592 |
return AudioFileClip(str(audio_path))
|
| 593 |
-
|
| 594 |
except Exception as e:
|
| 595 |
-
|
| 596 |
-
# Create
|
| 597 |
-
|
| 598 |
-
return AudioFileClip(duration=silence_duration)
|
| 599 |
|
| 600 |
def apply_video_effects(self, frame: np.ndarray, effect_params: dict) -> np.ndarray:
|
| 601 |
"""Apply various video effects to a frame"""
|
|
@@ -955,60 +887,152 @@ class EnhancedVideoGenerator:
|
|
| 955 |
msecs = int((seconds - int(seconds)) * 1000)
|
| 956 |
return f"{hours:02d}:{minutes:02d}:{secs:02d},{msecs:03d}"
|
| 957 |
|
| 958 |
-
def create_video(self,
|
| 959 |
-
|
|
|
|
| 960 |
try:
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
return output_path
|
|
|
|
| 982 |
except Exception as e:
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
if not images or not audio_file:
|
| 997 |
-
st.error("Please upload both images and audio.")
|
| 998 |
-
else:
|
| 999 |
-
try:
|
| 1000 |
-
generator = EnhancedVideoGenerator()
|
| 1001 |
-
image_paths = [img.name for img in images]
|
| 1002 |
-
for img in images:
|
| 1003 |
-
with open(img.name, "wb") as f:
|
| 1004 |
-
f.write(img.getbuffer())
|
| 1005 |
-
|
| 1006 |
-
# Generate video
|
| 1007 |
-
output = generator.create_video(image_paths, audio_file.name, duration, output_path)
|
| 1008 |
-
st.success(f"Video generated: {output}")
|
| 1009 |
-
st.video(output)
|
| 1010 |
-
except Exception as e:
|
| 1011 |
-
st.error(f"Error: {e}")
|
| 1012 |
|
| 1013 |
|
| 1014 |
|
|
|
|
| 6 |
from PIL import Image, ImageDraw, ImageFont
|
| 7 |
import tempfile
|
| 8 |
import os
|
| 9 |
+
from moviepy.editor import *
|
| 10 |
import numpy as np
|
| 11 |
from gtts import gTTS
|
| 12 |
import textwrap
|
|
|
|
| 457 |
return False
|
| 458 |
|
| 459 |
def generate_fallback_audio(self, script: str) -> AudioFileClip:
|
| 460 |
+
"""Generate fallback audio using gTTS"""
|
| 461 |
try:
|
| 462 |
audio_path = self.temp_dir / "voice.mp3"
|
| 463 |
+
tts = gTTS(text=script, lang='en', slow=False)
|
| 464 |
+
tts.save(str(audio_path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
return AudioFileClip(str(audio_path))
|
|
|
|
| 466 |
except Exception as e:
|
| 467 |
+
print(f"Fallback audio generation failed: {e}")
|
| 468 |
+
# Create silent audio clip
|
| 469 |
+
return AudioFileClip(str(audio_path)) if os.path.exists(str(audio_path)) else None
|
| 470 |
+
|
|
|
|
| 471 |
def scrape_pexels(self, query: str) -> List[str]:
|
| 472 |
urls = []
|
| 473 |
try:
|
|
|
|
| 518 |
|
| 519 |
|
| 520 |
def generate_fallback_audio(self, script: str) -> AudioFileClip:
|
| 521 |
+
"""Generate fallback audio using gTTS"""
|
| 522 |
try:
|
| 523 |
audio_path = self.temp_dir / "voice.mp3"
|
| 524 |
+
tts = gTTS(text=script, lang='en', slow=False)
|
| 525 |
+
tts.save(str(audio_path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
return AudioFileClip(str(audio_path))
|
|
|
|
| 527 |
except Exception as e:
|
| 528 |
+
print(f"Fallback audio generation failed: {e}")
|
| 529 |
+
# Create silent audio clip
|
| 530 |
+
return AudioFileClip(str(audio_path)) if os.path.exists(str(audio_path)) else None
|
|
|
|
| 531 |
|
| 532 |
def apply_video_effects(self, frame: np.ndarray, effect_params: dict) -> np.ndarray:
|
| 533 |
"""Apply various video effects to a frame"""
|
|
|
|
| 887 |
msecs = int((seconds - int(seconds)) * 1000)
|
| 888 |
return f"{hours:02d}:{minutes:02d}:{secs:02d},{msecs:03d}"
|
| 889 |
|
| 890 |
+
def create_video(self, script: str, style: str, duration: int, output_path: str, selected_images: List[str],
|
| 891 |
+
video_effects: dict = None, progress_callback: Callable[[float], None] = None) -> str:
|
| 892 |
+
"""Create video with selected images and effects"""
|
| 893 |
try:
|
| 894 |
+
# Initialize default effects if none provided
|
| 895 |
+
if video_effects is None:
|
| 896 |
+
video_effects = {
|
| 897 |
+
'zoom': 1.0,
|
| 898 |
+
'brightness': 1.0,
|
| 899 |
+
'contrast': 1.0,
|
| 900 |
+
'blur': False
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
# Process images with error handling
|
| 904 |
+
processed_images = []
|
| 905 |
+
total_images = len(selected_images)
|
| 906 |
+
|
| 907 |
+
for idx, img_url in enumerate(selected_images):
|
| 908 |
+
try:
|
| 909 |
+
response = requests.get(img_url, timeout=10)
|
| 910 |
+
response.raise_for_status()
|
| 911 |
+
img = Image.open(BytesIO(response.content))
|
| 912 |
+
img = img.convert('RGB')
|
| 913 |
+
img = img.resize((1920, 1080), Image.LANCZOS)
|
| 914 |
+
processed_images.append(img)
|
| 915 |
+
|
| 916 |
+
# Update progress (20% of total progress is for image processing)
|
| 917 |
+
if progress_callback:
|
| 918 |
+
progress = (idx + 1) / total_images * 20
|
| 919 |
+
progress_callback(progress)
|
| 920 |
+
|
| 921 |
+
except Exception as e:
|
| 922 |
+
self.logger.error(f"Error processing image {img_url}: {e}")
|
| 923 |
+
continue
|
| 924 |
+
|
| 925 |
+
if not processed_images:
|
| 926 |
+
raise ValueError("No valid images to process")
|
| 927 |
+
|
| 928 |
+
# Generate voice-over
|
| 929 |
+
audio = self.generate_fallback_audio(script)
|
| 930 |
+
if progress_callback:
|
| 931 |
+
progress_callback(30) # 30% progress after audio generation
|
| 932 |
+
|
| 933 |
+
# Calculate frames
|
| 934 |
+
fps = 30
|
| 935 |
+
total_frames = int(duration * fps)
|
| 936 |
+
frames_per_image = total_frames // len(processed_images)
|
| 937 |
+
|
| 938 |
+
# Create frames with effects
|
| 939 |
+
frames = []
|
| 940 |
+
frame_count = 0
|
| 941 |
+
|
| 942 |
+
# Apply zoom effect over time
|
| 943 |
+
zoom_range = np.linspace(1.0, video_effects['zoom'], frames_per_image)
|
| 944 |
+
|
| 945 |
+
for idx, img in enumerate(processed_images):
|
| 946 |
+
img_array = np.array(img)
|
| 947 |
+
|
| 948 |
+
# Calculate frames for this image
|
| 949 |
+
if idx == len(processed_images) - 1:
|
| 950 |
+
n_frames = total_frames - frame_count
|
| 951 |
+
else:
|
| 952 |
+
n_frames = min(frames_per_image, total_frames - frame_count)
|
| 953 |
+
|
| 954 |
+
# Generate frames with effects
|
| 955 |
+
for frame_idx in range(n_frames):
|
| 956 |
+
current_effects = video_effects.copy()
|
| 957 |
+
|
| 958 |
+
# Update zoom factor
|
| 959 |
+
if video_effects['zoom'] != 1.0:
|
| 960 |
+
current_effects['zoom'] = zoom_range[min(frame_idx, len(zoom_range)-1)]
|
| 961 |
+
|
| 962 |
+
# Apply effects to frame
|
| 963 |
+
frame = self.apply_video_effects(img_array.copy(), current_effects)
|
| 964 |
+
frames.append(frame)
|
| 965 |
+
frame_count += 1
|
| 966 |
+
|
| 967 |
+
# Update progress (30% to 70% is for frame generation)
|
| 968 |
+
if progress_callback and frame_count % 30 == 0:
|
| 969 |
+
progress = 30 + (frame_count / total_frames * 40)
|
| 970 |
+
progress_callback(progress)
|
| 971 |
+
|
| 972 |
+
# Add transition to next image if enabled
|
| 973 |
+
if idx < len(processed_images) - 1 and video_effects.get('transition_style') != 'None':
|
| 974 |
+
next_img_array = np.array(processed_images[idx + 1])
|
| 975 |
+
transition_frames = 15
|
| 976 |
+
|
| 977 |
+
for t in range(transition_frames):
|
| 978 |
+
if frame_count < total_frames:
|
| 979 |
+
alpha = t / transition_frames
|
| 980 |
+
transition_frame = cv2.addWeighted(
|
| 981 |
+
img_array, 1 - alpha,
|
| 982 |
+
next_img_array, alpha, 0
|
| 983 |
+
)
|
| 984 |
+
frames.append(transition_frame)
|
| 985 |
+
frame_count += 1
|
| 986 |
+
|
| 987 |
+
# Create video clip
|
| 988 |
+
clip = ImageSequenceClip(frames, fps=fps)
|
| 989 |
+
if progress_callback:
|
| 990 |
+
progress_callback(80) # 80% progress after creating clip
|
| 991 |
+
|
| 992 |
+
# Adjust audio duration
|
| 993 |
+
if audio.duration > clip.duration:
|
| 994 |
+
audio = audio.subclip(0, clip.duration)
|
| 995 |
+
elif audio.duration < clip.duration:
|
| 996 |
+
clip = clip.subclip(0, audio.duration)
|
| 997 |
+
|
| 998 |
+
# Combine video and audio
|
| 999 |
+
final_clip = clip.set_audio(audio)
|
| 1000 |
+
if progress_callback:
|
| 1001 |
+
progress_callback(90) # 90% progress after combining audio
|
| 1002 |
+
|
| 1003 |
+
# Ensure output directory exists
|
| 1004 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 1005 |
+
|
| 1006 |
+
# Write video file
|
| 1007 |
+
final_clip.write_videofile(
|
| 1008 |
+
output_path,
|
| 1009 |
+
fps=fps,
|
| 1010 |
+
codec='libx264',
|
| 1011 |
+
audio_codec='aac',
|
| 1012 |
+
ffmpeg_params=['-pix_fmt', 'yuv420p'],
|
| 1013 |
+
verbose=False,
|
| 1014 |
+
logger=None
|
| 1015 |
+
)
|
| 1016 |
+
|
| 1017 |
+
if progress_callback:
|
| 1018 |
+
progress_callback(100) # 100% progress after writing file
|
| 1019 |
+
|
| 1020 |
return output_path
|
| 1021 |
+
|
| 1022 |
except Exception as e:
|
| 1023 |
+
self.logger.error(f"Video creation failed: {str(e)}")
|
| 1024 |
+
raise
|
| 1025 |
+
finally:
|
| 1026 |
+
# Cleanup
|
| 1027 |
+
try:
|
| 1028 |
+
if 'clip' in locals():
|
| 1029 |
+
clip.close()
|
| 1030 |
+
if 'final_clip' in locals():
|
| 1031 |
+
final_clip.close()
|
| 1032 |
+
if 'audio' in locals():
|
| 1033 |
+
audio.close()
|
| 1034 |
+
except Exception as e:
|
| 1035 |
+
self.logger.error(f"Cleanup error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
|
| 1037 |
|
| 1038 |
|