mgbam commited on
Commit
03bb9f6
·
verified ·
1 Parent(s): e91404a

Update core/visual_engine.py

Browse files
Files changed (1) hide show
  1. core/visual_engine.py +33 -66
core/visual_engine.py CHANGED
@@ -1,7 +1,6 @@
1
  # core/visual_engine.py
2
  import tempfile
3
  import logging
4
- from pathlib import Path
5
  from PIL import Image, ImageDraw, ImageFont
6
  from moviepy.editor import ImageClip, concatenate_videoclips
7
  import os
@@ -12,76 +11,59 @@ logging.basicConfig(level=logging.INFO)
12
 
13
  class VisualEngine:
14
  def __init__(self, output_dir=None):
15
- """
16
- Initialize the visual engine with a safe output directory.
17
-
18
- If no output_dir is provided, creates a temporary directory in the system temp location.
19
- This avoids permission issues as /tmp is typically writable by all users.
20
- """
21
  self.output_dir = output_dir or self._create_temp_output_dir()
22
  logger.info(f"Using output directory: {self.output_dir}")
23
-
24
- # Ensure the directory exists and has proper permissions
25
  os.makedirs(self.output_dir, exist_ok=True)
26
- os.chmod(self.output_dir, 0o775) # Ensure writable by user and group
27
 
28
- # --- Font Setup ---
29
- self.font_filename = "arial.ttf"
30
  self.font_size_pil = 24
31
-
32
- # Try multiple font locations with fallbacks
33
- self.font = self._load_font_with_fallbacks()
34
 
35
  if self.font:
36
- logger.info(f"Successfully loaded font: {self.font_path}")
37
  else:
38
- logger.warning("Could not load any custom font. Falling back to default font.")
39
  self.font = ImageFont.load_default()
40
- self.font_size_pil = 11 # Adjust for default font
41
 
42
  def _create_temp_output_dir(self):
43
  """Create a temporary directory with appropriate permissions"""
44
  temp_dir = tempfile.mkdtemp(prefix="cinegen_media_")
45
- os.chmod(temp_dir, 0o775) # rwxrwxr-x permissions
46
  return temp_dir
47
 
48
- def _load_font_with_fallbacks(self):
49
- """Try multiple font locations with graceful fallback"""
50
- # List of possible font locations (Docker and local development)
51
- possible_font_paths = [
52
- # Docker container path
53
- f"/usr/local/share/fonts/truetype/mycustomfonts/{self.font_filename}",
54
- # Local development path (relative to script)
55
- os.path.join(os.path.dirname(__file__), "..", "assets", "fonts", self.font_filename),
56
- # System fonts as last resort
57
- f"/usr/share/fonts/truetype/{self.font_filename}",
58
- f"/usr/share/fonts/TTF/{self.font_filename}"
59
  ]
60
 
61
- for font_path in possible_font_paths:
 
62
  try:
63
- if os.path.exists(font_path):
64
- self.font_path = font_path
65
- return ImageFont.truetype(font_path, self.font_size_pil)
66
- except IOError as e:
67
- logger.warning(f"Could not load font from {font_path}: {str(e)}")
68
 
69
- return None
 
 
 
 
70
 
71
  def _get_text_dimensions(self, text_content, font_obj):
72
- """
73
- Gets the width and height of a single line of text with the given font.
74
- Returns (width, height).
75
- """
76
  if not text_content:
77
  return 0, self.font_size_pil
78
 
79
  try:
80
- # Modern Pillow (>=8.0.0)
81
  if hasattr(font_obj, 'getbbox'):
82
  bbox = font_obj.getbbox(text_content)
83
  return bbox[2] - bbox[0], bbox[3] - bbox[1]
84
- # Legacy Pillow
85
  elif hasattr(font_obj, 'getsize'):
86
  return font_obj.getsize(text_content)
87
  except Exception as e:
@@ -101,7 +83,7 @@ class VisualEngine:
101
  text_description = "No description provided"
102
 
103
  # Create text with wrapping
104
- lines = self._wrap_text(text_description, size[0] - 80, draw)
105
 
106
  # Calculate vertical position to center text
107
  _, line_height = self._get_text_dimensions("Tg", self.font)
@@ -118,15 +100,17 @@ class VisualEngine:
118
  # Save to output directory
119
  output_path = os.path.join(self.output_dir, filename)
120
  img.save(output_path)
121
- logger.debug(f"Created placeholder image: {output_path}")
122
  return output_path
123
 
124
  except Exception as e:
125
  logger.error(f"Error creating placeholder image: {str(e)}")
126
  return None
127
 
128
- def _wrap_text(self, text, max_width, draw):
129
  """Wrap text to fit within specified width"""
 
 
 
130
  words = text.split()
131
  lines = []
132
  current_line = []
@@ -144,8 +128,7 @@ class VisualEngine:
144
 
145
  # Handle very long words
146
  if self._get_text_dimensions(word, self.font)[0] > max_width:
147
- # Break word if needed
148
- while self._get_text_dimensions(''.join(current_line), self.font)[0] > max_width:
149
  current_line[0] = current_line[0][:-1]
150
 
151
  if current_line:
@@ -164,24 +147,11 @@ class VisualEngine:
164
  logger.error("No valid image paths found")
165
  return None
166
 
167
- logger.info(f"Creating video from {len(valid_paths)} images")
168
-
169
  try:
170
- clips = []
171
- for img_path in valid_paths:
172
- try:
173
- clip = ImageClip(img_path).set_duration(duration_per_image)
174
- clips.append(clip)
175
- except Exception as e:
176
- logger.error(f"Error processing {img_path}: {str(e)}")
177
-
178
- if not clips:
179
- return None
180
-
181
  video = concatenate_videoclips(clips, method="compose")
182
  output_path = os.path.join(self.output_dir, output_filename)
183
 
184
- # Write video file
185
  video.write_videofile(
186
  output_path,
187
  fps=fps,
@@ -190,7 +160,7 @@ class VisualEngine:
190
  temp_audiofile=os.path.join(self.output_dir, 'temp_audio.m4a'),
191
  remove_temp=True,
192
  threads=os.cpu_count() or 2,
193
- logger=None # Suppress moviepy's verbose output
194
  )
195
 
196
  # Clean up resources
@@ -198,11 +168,8 @@ class VisualEngine:
198
  clip.close()
199
  video.close()
200
 
201
- logger.info(f"Video created: {output_path}")
202
  return output_path
203
 
204
  except Exception as e:
205
  logger.error(f"Video creation failed: {str(e)}")
206
- if isinstance(e, OSError):
207
- logger.error("OSError: Check ffmpeg installation and file permissions")
208
  return None
 
1
  # core/visual_engine.py
2
  import tempfile
3
  import logging
 
4
  from PIL import Image, ImageDraw, ImageFont
5
  from moviepy.editor import ImageClip, concatenate_videoclips
6
  import os
 
11
 
12
  class VisualEngine:
13
  def __init__(self, output_dir=None):
 
 
 
 
 
 
14
  self.output_dir = output_dir or self._create_temp_output_dir()
15
  logger.info(f"Using output directory: {self.output_dir}")
 
 
16
  os.makedirs(self.output_dir, exist_ok=True)
 
17
 
18
+ # Font configuration
 
19
  self.font_size_pil = 24
20
+ self.font = self._load_system_font()
 
 
21
 
22
  if self.font:
23
+ logger.info(f"Using font: {self.font.path if hasattr(self.font, 'path') else 'default'}")
24
  else:
25
+ logger.warning("Could not load any suitable font. Falling back to default font.")
26
  self.font = ImageFont.load_default()
27
+ self.font_size_pil = 11
28
 
29
  def _create_temp_output_dir(self):
30
  """Create a temporary directory with appropriate permissions"""
31
  temp_dir = tempfile.mkdtemp(prefix="cinegen_media_")
32
+ os.chmod(temp_dir, 0o775)
33
  return temp_dir
34
 
35
+ def _load_system_font(self):
36
+ """Load the best available system font"""
37
+ font_names = [
38
+ "DejaVuSans.ttf", # Common in Linux
39
+ "FreeSans.ttf", # From fonts-freefont-ttf
40
+ "LiberationSans-Regular.ttf", # Red Hat font
41
+ "Arial.ttf", # Sometimes available
42
+ "Vera.ttf" # Bitstream Vera
 
 
 
43
  ]
44
 
45
+ # Try to load each font in order
46
+ for font_name in font_names:
47
  try:
48
+ return ImageFont.truetype(font_name, self.font_size_pil)
49
+ except IOError:
50
+ continue
 
 
51
 
52
+ # Try system default sans-serif as last resort
53
+ try:
54
+ return ImageFont.truetype("sans-serif", self.font_size_pil)
55
+ except:
56
+ return None
57
 
58
  def _get_text_dimensions(self, text_content, font_obj):
59
+ """Get text dimensions with modern Pillow methods"""
 
 
 
60
  if not text_content:
61
  return 0, self.font_size_pil
62
 
63
  try:
 
64
  if hasattr(font_obj, 'getbbox'):
65
  bbox = font_obj.getbbox(text_content)
66
  return bbox[2] - bbox[0], bbox[3] - bbox[1]
 
67
  elif hasattr(font_obj, 'getsize'):
68
  return font_obj.getsize(text_content)
69
  except Exception as e:
 
83
  text_description = "No description provided"
84
 
85
  # Create text with wrapping
86
+ lines = self._wrap_text(text_description, size[0] - 80)
87
 
88
  # Calculate vertical position to center text
89
  _, line_height = self._get_text_dimensions("Tg", self.font)
 
100
  # Save to output directory
101
  output_path = os.path.join(self.output_dir, filename)
102
  img.save(output_path)
 
103
  return output_path
104
 
105
  except Exception as e:
106
  logger.error(f"Error creating placeholder image: {str(e)}")
107
  return None
108
 
109
+ def _wrap_text(self, text, max_width):
110
  """Wrap text to fit within specified width"""
111
+ if not text:
112
+ return ["(No text)"]
113
+
114
  words = text.split()
115
  lines = []
116
  current_line = []
 
128
 
129
  # Handle very long words
130
  if self._get_text_dimensions(word, self.font)[0] > max_width:
131
+ while current_line and self._get_text_dimensions(''.join(current_line), self.font)[0] > max_width:
 
132
  current_line[0] = current_line[0][:-1]
133
 
134
  if current_line:
 
147
  logger.error("No valid image paths found")
148
  return None
149
 
 
 
150
  try:
151
+ clips = [ImageClip(img_path).set_duration(duration_per_image) for img_path in valid_paths]
 
 
 
 
 
 
 
 
 
 
152
  video = concatenate_videoclips(clips, method="compose")
153
  output_path = os.path.join(self.output_dir, output_filename)
154
 
 
155
  video.write_videofile(
156
  output_path,
157
  fps=fps,
 
160
  temp_audiofile=os.path.join(self.output_dir, 'temp_audio.m4a'),
161
  remove_temp=True,
162
  threads=os.cpu_count() or 2,
163
+ logger=None
164
  )
165
 
166
  # Clean up resources
 
168
  clip.close()
169
  video.close()
170
 
 
171
  return output_path
172
 
173
  except Exception as e:
174
  logger.error(f"Video creation failed: {str(e)}")
 
 
175
  return None