File size: 6,455 Bytes
287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f 287c9ca b97795f |
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 |
# core/visual_engine.py
from PIL import Image, ImageDraw, ImageFont
from moviepy.editor import ImageClip, concatenate_videoclips
import os
class VisualEngine:
def __init__(self, output_dir="generated_media"):
self.output_dir = output_dir
os.makedirs(self.output_dir, exist_ok=True)
try:
# Try to load Arial font. Ensure it's installed in your Docker image.
# Common paths: "arial.ttf", "/usr/share/fonts/truetype/msttcorefonts/Arial.ttf"
# If using a custom font, ensure you COPY it into the Docker image and provide the correct path.
self.font_path = "arial.ttf" # This assumes Arial is in a standard font path or current dir
self.font_size_pil = 24 # For Pillow text drawing
self.font = ImageFont.truetype(self.font_path, self.font_size_pil)
print(f"Successfully loaded font: {self.font_path} with size {self.font_size_pil}")
except IOError:
print(f"Warning: Could not load font '{self.font_path}'. Falling back to default font.")
self.font = ImageFont.load_default()
self.font_size_pil = 10 # Default font is smaller, adjust size for calculations
def _get_text_dimensions(self, text_content, font_obj):
"""
Gets the width and height of a single line of text with the given font.
Returns (width, height).
"""
if hasattr(font_obj, 'getbbox'): # For newer Pillow versions (>=8.0.0)
# getbbox returns (left, top, right, bottom)
bbox = font_obj.getbbox(text_content)
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
return width, height
elif hasattr(font_obj, 'getsize'): # For older Pillow versions
return font_obj.getsize(text_content)
else: # Fallback for very basic font objects (like default font)
# Estimate: average char width * num_chars, fixed height
# This is a rough estimate.
avg_char_width = self.font_size_pil * 0.6
height_estimate = self.font_size_pil * 1.2
return int(len(text_content) * avg_char_width), int(height_estimate)
def create_placeholder_image(self, text_description, filename, size=(1280, 720)):
img = Image.new('RGB', size, color=(73, 109, 137)) # Blueish background
draw = ImageDraw.Draw(img)
padding = 40
max_text_width = size[0] - (2 * padding)
lines = []
words = text_description.split()
current_line = ""
for word in words:
test_line = current_line + word + " "
line_width, _ = self._get_text_dimensions(test_line.strip(), self.font)
if line_width <= max_text_width:
current_line = test_line
else:
lines.append(current_line.strip())
current_line = word + " "
lines.append(current_line.strip()) # Add the last line
# Calculate starting y position to center the text block
# Use the height from the first line as an estimate for line height
_, single_line_height = self._get_text_dimensions("Tg", self.font) # Get height of a typical line
line_spacing_factor = 1.2 # Adjust for spacing between lines
total_text_height = len(lines) * single_line_height * line_spacing_factor
y_text = (size[1] - total_text_height) / 2
if y_text < padding: # Ensure text doesn't start too high if too much text
y_text = padding
for line in lines:
line_width, _ = self._get_text_dimensions(line, self.font)
x_text = (size[0] - line_width) / 2
# Corrected ImageDraw.text() call with explicit keyword arguments
draw.text(
xy=(x_text, y_text),
text=line,
fill=(255, 255, 0), # Yellow text
font=self.font
)
y_text += single_line_height * line_spacing_factor
filepath = os.path.join(self.output_dir, filename)
try:
img.save(filepath)
print(f"Placeholder image saved: {filepath}")
except Exception as e:
print(f"Error saving image {filepath}: {e}")
return None
return filepath
def create_video_from_images(self, image_paths, output_filename="final_video.mp4", fps=1, duration_per_image=3):
if not image_paths:
print("No images provided to create video.")
return None
valid_image_paths = [p for p in image_paths if p and os.path.exists(p)]
if not valid_image_paths:
print("No valid image paths found to create video.")
return None
print(f"Creating video from images: {valid_image_paths}")
try:
clips = [ImageClip(m).set_duration(duration_per_image) for m in valid_image_paths]
if not clips:
print("Could not create ImageClips from the provided image paths.")
return None
video_clip = concatenate_videoclips(clips, method="compose")
output_path = os.path.join(self.output_dir, output_filename)
# Using recommended parameters for write_videofile, especially in headless environments
video_clip.write_videofile(
output_path,
fps=fps,
codec='libx264', # A common and good codec
audio_codec='aac', # If you ever add audio
temp_audiofile='temp-audio.m4a', # For temporary audio processing
remove_temp=True, # Clean up temporary audio file
threads=os.cpu_count() or 2, # Use available CPUs or default to 2
logger=None # Suppress verbose ffmpeg output, use 'bar' for progress
)
print(f"Video successfully created: {output_path}")
return output_path
except Exception as e:
print(f"Error creating video: {e}")
# Print more details if it's an OSError, which can happen with ffmpeg issues
if isinstance(e, OSError):
print("OSError during video creation. This often indicates an issue with ffmpeg.")
print("Ensure ffmpeg is correctly installed and accessible in the environment PATH.")
return None |