File size: 6,681 Bytes
287c9ca
5470dfc
 
287c9ca
 
 
 
5470dfc
 
 
 
287c9ca
5470dfc
 
 
287c9ca
50c620f
03bb9f6
50c620f
03bb9f6
5470dfc
 
03bb9f6
5470dfc
03bb9f6
b97795f
03bb9f6
5470dfc
 
 
 
03bb9f6
5470dfc
 
03bb9f6
 
 
 
 
 
 
 
5470dfc
 
03bb9f6
 
5470dfc
03bb9f6
 
 
5470dfc
03bb9f6
 
 
 
 
b97795f
 
03bb9f6
5470dfc
 
50c620f
 
5470dfc
50c620f
5470dfc
 
 
50c620f
5470dfc
 
 
 
 
287c9ca
 
5470dfc
 
 
 
 
 
 
 
 
03bb9f6
5470dfc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b97795f
03bb9f6
5470dfc
03bb9f6
 
 
5470dfc
287c9ca
5470dfc
50c620f
 
5470dfc
 
50c620f
5470dfc
 
 
 
 
 
 
 
 
03bb9f6
5470dfc
287c9ca
5470dfc
 
 
 
b97795f
287c9ca
5470dfc
287c9ca
5470dfc
287c9ca
5470dfc
 
 
 
287c9ca
5470dfc
b97795f
03bb9f6
5470dfc
b97795f
287c9ca
5470dfc
b97795f
 
50c620f
5470dfc
 
50c620f
5470dfc
03bb9f6
b97795f
5470dfc
 
 
 
 
 
b97795f
5470dfc
b97795f
5470dfc
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# core/visual_engine.py
import tempfile
import logging
from PIL import Image, ImageDraw, ImageFont
from moviepy.editor import ImageClip, concatenate_videoclips
import os

# Set up logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

class VisualEngine:
    def __init__(self, output_dir=None):
        self.output_dir = output_dir or self._create_temp_output_dir()
        logger.info(f"Using output directory: {self.output_dir}")
        os.makedirs(self.output_dir, exist_ok=True)
        
        # Font configuration
        self.font_size_pil = 24
        self.font = self._load_system_font()
        
        if self.font:
            logger.info(f"Using font: {self.font.path if hasattr(self.font, 'path') else 'default'}")
        else:
            logger.warning("Could not load any suitable font. Falling back to default font.")
            self.font = ImageFont.load_default()
            self.font_size_pil = 11

    def _create_temp_output_dir(self):
        """Create a temporary directory with appropriate permissions"""
        temp_dir = tempfile.mkdtemp(prefix="cinegen_media_")
        os.chmod(temp_dir, 0o775)
        return temp_dir

    def _load_system_font(self):
        """Load the best available system font"""
        font_names = [
            "DejaVuSans.ttf",       # Common in Linux
            "FreeSans.ttf",          # From fonts-freefont-ttf
            "LiberationSans-Regular.ttf",  # Red Hat font
            "Arial.ttf",             # Sometimes available
            "Vera.ttf"               # Bitstream Vera
        ]
        
        # Try to load each font in order
        for font_name in font_names:
            try:
                return ImageFont.truetype(font_name, self.font_size_pil)
            except IOError:
                continue
        
        # Try system default sans-serif as last resort
        try:
            return ImageFont.truetype("sans-serif", self.font_size_pil)
        except:
            return None

    def _get_text_dimensions(self, text_content, font_obj):
        """Get text dimensions with modern Pillow methods"""
        if not text_content:
            return 0, self.font_size_pil

        try:
            if hasattr(font_obj, 'getbbox'):
                bbox = font_obj.getbbox(text_content)
                return bbox[2] - bbox[0], bbox[3] - bbox[1]
            elif hasattr(font_obj, 'getsize'):
                return font_obj.getsize(text_content)
        except Exception as e:
            logger.warning(f"Error measuring text: {str(e)}")
        
        # Fallback calculation
        avg_char_width = self.font_size_pil * 0.6
        return int(len(text_content) * avg_char_width), self.font_size_pil

    def create_placeholder_image(self, text_description, filename, size=(1280, 720)):
        """Create placeholder image with wrapped text"""
        try:
            img = Image.new('RGB', size, color=(30, 30, 60))
            draw = ImageDraw.Draw(img)
            
            if not text_description:
                text_description = "No description provided"
                
            # Create text with wrapping
            lines = self._wrap_text(text_description, size[0] - 80)
            
            # Calculate vertical position to center text
            _, line_height = self._get_text_dimensions("Tg", self.font)
            total_height = len(lines) * line_height * 1.3
            y_pos = max(40, (size[1] - total_height) / 2)
            
            # Draw each line
            for line in lines:
                line_width, _ = self._get_text_dimensions(line, self.font)
                x_pos = (size[0] - line_width) / 2
                draw.text((x_pos, y_pos), line, fill=(220, 220, 150), font=self.font)
                y_pos += line_height * 1.3
                
            # Save to output directory
            output_path = os.path.join(self.output_dir, filename)
            img.save(output_path)
            return output_path
            
        except Exception as e:
            logger.error(f"Error creating placeholder image: {str(e)}")
            return None

    def _wrap_text(self, text, max_width):
        """Wrap text to fit within specified width"""
        if not text:
            return ["(No text)"]
            
        words = text.split()
        lines = []
        current_line = []
        
        for word in words:
            test_line = ' '.join(current_line + [word])
            test_width, _ = self._get_text_dimensions(test_line, self.font)
            
            if test_width <= max_width:
                current_line.append(word)
            else:
                if current_line:
                    lines.append(' '.join(current_line))
                current_line = [word]
                
                # Handle very long words
                if self._get_text_dimensions(word, self.font)[0] > max_width:
                    while current_line and self._get_text_dimensions(''.join(current_line), self.font)[0] > max_width:
                        current_line[0] = current_line[0][:-1]
        
        if current_line:
            lines.append(' '.join(current_line))
            
        return lines or ["(Text rendering error)"]

    def create_video_from_images(self, image_paths, output_filename="final_video.mp4", fps=1, duration_per_image=3):
        """Create video from sequence of images"""
        if not image_paths:
            logger.error("No images provided for video creation")
            return None
            
        valid_paths = [p for p in image_paths if p and os.path.exists(p)]
        if not valid_paths:
            logger.error("No valid image paths found")
            return None
            
        try:
            clips = [ImageClip(img_path).set_duration(duration_per_image) for img_path in valid_paths]
            video = concatenate_videoclips(clips, method="compose")
            output_path = os.path.join(self.output_dir, output_filename)
            
            video.write_videofile(
                output_path,
                fps=fps,
                codec='libx264',
                audio_codec='aac',
                temp_audiofile=os.path.join(self.output_dir, 'temp_audio.m4a'),
                remove_temp=True,
                threads=os.cpu_count() or 2,
                logger=None
            )
            
            # Clean up resources
            for clip in clips:
                clip.close()
            video.close()
            
            return output_path
            
        except Exception as e:
            logger.error(f"Video creation failed: {str(e)}")
            return None