mgbam commited on
Commit
940b1d9
·
verified ·
1 Parent(s): da83876

Update core/visual_engine.py

Browse files
Files changed (1) hide show
  1. core/visual_engine.py +190 -148
core/visual_engine.py CHANGED
@@ -4,7 +4,7 @@ import base64
4
  import mimetypes
5
  import numpy as np
6
  import os
7
- import openai
8
  import requests
9
  import io
10
  import time
@@ -15,17 +15,17 @@ from moviepy.editor import (ImageClip, VideoFileClip, concatenate_videoclips, Te
15
  CompositeVideoClip, AudioFileClip)
16
  import moviepy.video.fx.all as vfx
17
 
18
- try:
19
- if hasattr(Image, 'Resampling') and hasattr(Image.Resampling, 'LANCZOS'):
20
  if not hasattr(Image, 'ANTIALIAS'): Image.ANTIALIAS = Image.Resampling.LANCZOS
21
- elif hasattr(Image, 'LANCZOS'):
22
  if not hasattr(Image, 'ANTIALIAS'): Image.ANTIALIAS = Image.LANCZOS
23
  elif not hasattr(Image, 'ANTIALIAS'):
24
  print("WARNING: Pillow version lacks common Resampling or ANTIALIAS. MoviePy effects might fail.")
25
  except Exception as e_mp: print(f"WARNING: ANTIALIAS monkey-patch error: {e_mp}")
26
 
27
  logger = logging.getLogger(__name__)
28
- # logger.setLevel(logging.DEBUG)
29
 
30
  ELEVENLABS_CLIENT_IMPORTED = False; ElevenLabsAPIClient = None; Voice = None; VoiceSettings = None
31
  try:
@@ -42,163 +42,199 @@ try:
42
  logger.info("RunwayML SDK imported.")
43
  except Exception as e_rwy_imp: logger.warning(f"RunwayML SDK import failed: {e_rwy_imp}. RunwayML disabled.")
44
 
 
45
  class VisualEngine:
46
  DEFAULT_FONT_SIZE_PIL = 10; PREFERRED_FONT_SIZE_PIL = 20
47
  VIDEO_OVERLAY_FONT_SIZE = 30; VIDEO_OVERLAY_FONT_COLOR = 'white'
48
  DEFAULT_MOVIEPY_FONT = 'DejaVu-Sans-Bold'; PREFERRED_MOVIEPY_FONT = 'Liberation-Sans-Bold'
49
 
 
50
  def __init__(self, output_dir="temp_cinegen_media", default_elevenlabs_voice_id="Rachel"):
51
  self.output_dir = output_dir
52
  try:
53
  os.makedirs(self.output_dir, exist_ok=True)
54
  logger.info(f"VisualEngine output directory set/ensured: {os.path.abspath(self.output_dir)}")
55
- except Exception as e_mkdir:
56
- logger.error(f"CRITICAL: Failed to create output directory '{self.output_dir}': {e_mkdir}", exc_info=True)
57
- # This is a critical failure; the app might not be able to save any files.
58
- # Consider raising the exception or setting a clear failure state for the engine.
59
- # raise OSError(f"Could not create output directory: {self.output_dir}") from e_mkdir
 
 
 
 
60
 
61
  self.font_filename_pil_preference = "DejaVuSans-Bold.ttf"
62
- font_paths = [ self.font_filename_pil_preference, f"/usr/share/fonts/truetype/dejavu/{self.font_filename_pil_preference}", f"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", f"/System/Library/Fonts/Supplemental/Arial.ttf", f"C:/Windows/Fonts/arial.ttf", f"/usr/local/share/fonts/truetype/mycustomfonts/arial.ttf"]
63
- self.resolved_font_path_pil = next((p for p in font_paths if os.path.exists(p)), None)
64
- self.active_font_pil = ImageFont.load_default(); self.active_font_size_pil = self.DEFAULT_FONT_SIZE_PIL; self.active_moviepy_font_name = self.DEFAULT_MOVIEPY_FONT
 
 
 
 
65
  if self.resolved_font_path_pil:
66
  try:
67
  self.active_font_pil = ImageFont.truetype(self.resolved_font_path_pil, self.PREFERRED_FONT_SIZE_PIL)
68
  self.active_font_size_pil = self.PREFERRED_FONT_SIZE_PIL
69
  logger.info(f"Pillow font loaded: {self.resolved_font_path_pil} at size {self.active_font_size_pil}.")
70
  self.active_moviepy_font_name = 'DejaVu-Sans-Bold' if "dejavu" in self.resolved_font_path_pil.lower() else ('Liberation-Sans-Bold' if "liberation" in self.resolved_font_path_pil.lower() else self.DEFAULT_MOVIEPY_FONT)
71
- except IOError as e_font: logger.error(f"Pillow font IOError '{self.resolved_font_path_pil}': {e_font}. Using default.")
72
- else: logger.warning("Preferred Pillow font not found. Using default.")
73
 
74
  self.openai_api_key = None; self.USE_AI_IMAGE_GENERATION = False; self.dalle_model = "dall-e-3"; self.image_size_dalle3 = "1792x1024"
75
  self.video_frame_size = (1280, 720)
 
76
  self.elevenlabs_api_key = None; self.USE_ELEVENLABS = False; self.elevenlabs_client_instance = None
77
- self.elevenlabs_voice_id = default_elevenlabs_voice_id
78
  logger.info(f"VisualEngine __init__: ElevenLabs Voice ID initially set to: {self.elevenlabs_voice_id}")
 
79
  if VoiceSettings and ELEVENLABS_CLIENT_IMPORTED: self.elevenlabs_voice_settings_obj = VoiceSettings(stability=0.60, similarity_boost=0.80, style=0.15, use_speaker_boost=True)
80
  else: self.elevenlabs_voice_settings_obj = None
 
81
  self.pexels_api_key = None; self.USE_PEXELS = False
82
  self.runway_api_key = None; self.USE_RUNWAYML = False; self.runway_ml_sdk_client_instance = None
 
83
  if RUNWAYML_SDK_IMPORTED and RunwayMLAPIClientClass and os.getenv("RUNWAYML_API_SECRET"):
84
- try: self.runway_ml_sdk_client_instance = RunwayMLAPIClientClass(); self.USE_RUNWAYML = True; logger.info("RunwayML Client init from env var at startup.")
85
- except Exception as e_rwy_init: logger.error(f"Initial RunwayML client init failed: {e_rwy_init}"); self.USE_RUNWAYML = False
86
- logger.info("VisualEngine __init__ sequence complete.")
 
87
 
88
  def set_openai_api_key(self, api_key_value): self.openai_api_key = api_key_value; self.USE_AI_IMAGE_GENERATION = bool(api_key_value); logger.info(f"DALL-E status: {'Ready' if self.USE_AI_IMAGE_GENERATION else 'Disabled'}")
 
89
  def set_elevenlabs_api_key(self, api_key_value, voice_id_from_secret=None):
90
  self.elevenlabs_api_key = api_key_value
91
  if voice_id_from_secret: self.elevenlabs_voice_id = voice_id_from_secret; logger.info(f"11L Voice ID updated via set_elevenlabs_api_key to: {self.elevenlabs_voice_id}")
 
92
  if api_key_value and ELEVENLABS_CLIENT_IMPORTED and ElevenLabsAPIClient:
93
- try: self.elevenlabs_client_instance = ElevenLabsAPIClient(api_key=api_key_value); self.USE_ELEVENLABS = bool(self.elevenlabs_client_instance); logger.info(f"11L Client: {'Ready' if self.USE_ELEVENLABS else 'Failed'} (Using Voice: {self.elevenlabs_voice_id})")
94
- except Exception as e_11l_setkey_init: logger.error(f"11L client init error: {e_11l_setkey_init}. Disabled.", exc_info=True); self.USE_ELEVENLABS=False; self.elevenlabs_client_instance=None
95
- else: self.USE_ELEVENLABS = False; logger.info(f"11L Disabled (API key not provided or SDK issue).")
 
96
  def set_pexels_api_key(self, api_key_value): self.pexels_api_key = api_key_value; self.USE_PEXELS = bool(api_key_value); logger.info(f"Pexels status: {'Ready' if self.USE_PEXELS else 'Disabled'}")
 
97
  def set_runway_api_key(self, api_key_value):
98
  self.runway_api_key = api_key_value
99
  if api_key_value:
100
  if RUNWAYML_SDK_IMPORTED and RunwayMLAPIClientClass:
101
- if not self.runway_ml_sdk_client_instance:
102
  try:
103
- original_env_secret = os.getenv("RUNWAYML_API_SECRET")
104
- if not original_env_secret: os.environ["RUNWAYML_API_SECRET"] = api_key_value; logger.info("Temporarily set RUNWAYML_API_SECRET from provided key for SDK client init.")
105
  self.runway_ml_sdk_client_instance = RunwayMLAPIClientClass(); self.USE_RUNWAYML = True; logger.info("RunwayML Client initialized successfully via set_runway_api_key.")
106
- if not original_env_secret: del os.environ["RUNWAYML_API_SECRET"]; logger.info("Cleared temporary RUNWAYML_API_SECRET environment variable.")
107
- except Exception as e_runway_setkey_init: logger.error(f"RunwayML Client initialization in set_runway_api_key failed: {e_runway_setkey_init}", exc_info=True); self.USE_RUNWAYML=False;self.runway_ml_sdk_client_instance=None
108
  else: self.USE_RUNWAYML = True; logger.info("RunwayML Client was already initialized (likely from environment variable). API key stored.")
109
  else: logger.warning("RunwayML SDK not imported. API key stored, but current integration relies on SDK. Service effectively disabled."); self.USE_RUNWAYML = False
110
  else: self.USE_RUNWAYML = False; self.runway_ml_sdk_client_instance = None; logger.info("RunwayML Service Disabled (no API key provided).")
111
 
112
- def _image_to_data_uri(self, image_path):
 
 
113
  try:
114
- mime_type, _ = mimetypes.guess_type(image_path)
115
- if not mime_type: ext = os.path.splitext(image_path)[1].lower(); mime_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"}; mime_type = mime_map.get(ext, "application/octet-stream");
116
- if mime_type == "application/octet-stream": logger.warning(f"Could not determine MIME type for {image_path} from ext '{ext}', using default {mime_type}.")
117
- with open(image_path, "rb") as image_file_handle: image_binary_data = image_file_handle.read()
118
- encoded_base64_string = base64.b64encode(image_binary_data).decode('utf-8')
119
- data_uri_string = f"data:{mime_type};base64,{encoded_base64_string}"; logger.debug(f"Data URI for {os.path.basename(image_path)} (MIME:{mime_type}): {data_uri_string[:100]}..."); return data_uri_string
120
- except FileNotFoundError: logger.error(f"Img not found {image_path} for data URI."); return None
121
- except Exception as e_data_uri: logger.error(f"Error converting {image_path} to data URI:{e_data_uri}", exc_info=True); return None
 
 
 
122
 
123
- def _map_resolution_to_runway_ratio(self, width, height):
124
- ratio_str=f"{width}:{height}";supported_ratios_gen4=["1280:720","720:1280","1104:832","832:1104","960:960","1584:672"];
125
- if ratio_str in supported_ratios_gen4:return ratio_str
126
- logger.warning(f"Res {ratio_str} not in Gen-4 list. Default 1280:720 for Runway.");return "1280:720"
127
 
128
- def _get_text_dimensions(self, text_content, font_object_pil):
129
- default_h = getattr(font_object_pil, 'size', self.active_font_size_pil)
130
- if not text_content: return 0, default_h
131
  try:
132
- if hasattr(font_object_pil,'getbbox'):bbox=font_object_pil.getbbox(text_content);w=bbox[2]-bbox[0];h=bbox[3]-bbox[1]; return w, h if h > 0 else default_h
133
- elif hasattr(font_object_pil,'getsize'):w,h=font_object_pil.getsize(text_content); return w, h if h > 0 else default_h
134
- else: return int(len(text_content)*default_h*0.6),int(default_h*1.2)
135
- except Exception as e_getdim: logger.warning(f"Error in _get_text_dimensions: {e_getdim}"); return int(len(text_content)*self.active_font_size_pil*0.6),int(self.active_font_size_pil*1.2)
136
 
137
- def _create_placeholder_image_content(self,text_description,filename,size=None):
138
- if size is None: size = self.video_frame_size
139
- img = Image.new('RGB', size, color=(20, 20, 40)); d_draw = ImageDraw.Draw(img); padding = 25
140
- max_w_text = size[0] - (2 * padding); lines_out = []
141
- if not text_description: text_description = "(Placeholder Image)"
142
- words_in_desc = text_description.split(); current_line_buf = ""
143
- for word_idx_loop, word_val in enumerate(words_in_desc):
144
- prospective_add_str = word_val + (" " if word_idx_loop < len(words_in_desc) - 1 else "")
145
- test_line_str = current_line_buf + prospective_add_str
146
- current_w_val, _ = self._get_text_dimensions(test_line_str, self.active_font_pil)
147
- if current_w_val == 0 and test_line_str.strip(): current_w_val = len(test_line_str) * (self.active_font_size_pil * 0.6)
148
- if current_w_val <= max_w_text: current_line_buf = test_line_str
 
149
  else:
150
- if current_line_buf.strip(): lines_out.append(current_line_buf.strip())
151
- current_line_buf = prospective_add_str
152
- if current_line_buf.strip(): lines_out.append(current_line_buf.strip())
153
- if not lines_out and text_description:
154
- avg_char_w_val, _ = self._get_text_dimensions("W", self.active_font_pil); avg_char_w_val = avg_char_w_val or (self.active_font_size_pil * 0.6)
155
- chars_p_line = int(max_w_text / avg_char_w_val) if avg_char_w_val > 0 else 20
156
- lines_out.append(text_description[:chars_p_line] + ("..." if len(text_description) > chars_p_line else ""))
157
- elif not lines_out: lines_out.append("(Placeholder Error)")
158
- _, single_line_h_val = self._get_text_dimensions("Ay", self.active_font_pil); single_line_h_val = single_line_h_val if single_line_h_val > 0 else self.active_font_size_pil + 2
159
- max_lines_disp = min(len(lines_out), (size[1] - (2 * padding)) // (single_line_h_val + 2)) if single_line_h_val > 0 else 1; max_lines_disp = max(1, max_lines_disp)
160
- y_pos_text = padding + (size[1] - (2 * padding) - max_lines_disp * (single_line_h_val + 2)) / 2.0
161
- for i_ln in range(max_lines_disp):
162
- line_content_str = lines_out[i_ln]; line_w_px, _ = self._get_text_dimensions(line_content_str, self.active_font_pil)
163
- if line_w_px == 0 and line_content_str.strip(): line_w_px = len(line_content_str) * (self.active_font_size_pil * 0.6)
164
- x_pos_text = (size[0] - line_w_px) / 2.0
165
- try: d_draw.text((x_pos_text, y_pos_text), line_content_str, font=self.active_font_pil, fill=(200, 200, 180))
166
- except Exception as e_draw_ph: logger.error(f"Pillow d.text error: {e_draw_ph} for '{line_content_str}'")
167
- y_pos_text += single_line_h_val + 2
168
- if i_ln == 6 and max_lines_disp > 7:
169
- try: d_draw.text((x_pos_text, y_pos_text), "...", font=self.active_font_pil, fill=(200, 200, 180))
170
- except Exception as e_elps_ph: logger.error(f"Pillow d.text ellipsis error: {e_elps_ph}"); break
171
- filepath_ph_img = os.path.join(self.output_dir, filename)
172
- try: img.save(filepath_ph_img); return filepath_ph_img
173
- except Exception as e_save_ph_img: logger.error(f"Saving placeholder image '{filepath_ph_img}' error: {e_save_ph_img}", exc_info=True); return None
174
 
175
- def _search_pexels_image(self, query_str_px, output_fn_base_px):
176
  if not self.USE_PEXELS or not self.pexels_api_key: return None
177
- http_headers_px = {"Authorization": self.pexels_api_key}
178
- http_params_px = {"query": query_str_px, "per_page": 1, "orientation": "landscape", "size": "large2x"}
179
- base_name_for_pexels_img, _ = os.path.splitext(output_fn_base_px)
180
- pexels_filename_output = base_name_for_pexels_img + f"_pexels_{random.randint(1000,9999)}.jpg"
181
- filepath_for_pexels_img = os.path.join(self.output_dir, pexels_filename_output)
182
  try:
183
- logger.info(f"Pexels: Searching for '{query_str_px}'")
184
- effective_query_for_pexels = " ".join(query_str_px.split()[:5])
185
- http_params_px["query"] = effective_query_for_pexels
186
- response_from_pexels = requests.get("https://api.pexels.com/v1/search", headers=http_headers_px, params=http_params_px, timeout=20)
187
- response_from_pexels.raise_for_status()
188
- data_from_pexels = response_from_pexels.json()
189
- if data_from_pexels.get("photos") and len(data_from_pexels["photos"]) > 0:
190
- photo_details_item_px = data_from_pexels["photos"][0]
191
- photo_url_item_px = photo_details_item_px.get("src", {}).get("large2x")
192
- if not photo_url_item_px: logger.warning(f"Pexels: 'large2x' URL missing for '{effective_query_for_pexels}'. Details: {photo_details_item_px}"); return None
193
- image_response_get_px = requests.get(photo_url_item_px, timeout=60); image_response_get_px.raise_for_status()
194
- img_pil_data_from_pexels = Image.open(io.BytesIO(image_response_get_px.content))
195
- if img_pil_data_from_pexels.mode != 'RGB': img_pil_data_from_pexels = img_pil_data_from_pexels.convert('RGB')
196
- img_pil_data_from_pexels.save(filepath_for_pexels_img); logger.info(f"Pexels: Image saved to {filepath_for_pexels_img}"); return filepath_for_pexels_img
197
- else: logger.info(f"Pexels: No photos for '{effective_query_for_pexels}'."); return None
198
- except requests.exceptions.RequestException as e_req_px_loop: logger.error(f"Pexels: RequestException for '{query_str_px}': {e_req_px_loop}", exc_info=False); return None
199
- except Exception as e_px_gen_loop: logger.error(f"Pexels: General error for '{query_str_px}': {e_px_gen_loop}", exc_info=True); return None
 
 
 
 
 
 
 
 
 
 
200
 
201
  def _generate_video_clip_with_runwayml(self, motion_prompt_rwy, input_img_path_rwy, scene_id_base_fn_rwy, duration_s_rwy=5):
 
202
  if not self.USE_RUNWAYML or not self.runway_ml_sdk_client_instance: logger.warning("RunwayML skip: Not enabled/client not init."); return None
203
  if not input_img_path_rwy or not os.path.exists(input_img_path_rwy): logger.error(f"Runway Gen-4 needs input img. Invalid: {input_img_path_rwy}"); return None
204
  img_data_uri_rwy = self._image_to_data_uri(input_img_path_rwy)
@@ -228,18 +264,18 @@ class VisualEngine:
228
  except AttributeError as e_rwy_sdk_attr: logger.error(f"RunwayML SDK AttrError:{e_rwy_sdk_attr}. SDK methods changed?",exc_info=True);return None
229
  except Exception as e_rwy_general: logger.error(f"Runway Gen-4 API error:{e_rwy_general}",exc_info=True);return None
230
 
231
- def _create_placeholder_video_content(self, text_desc_ph, filename_ph, duration_ph=4, size_ph=None):
232
- if size_ph is None: size_ph = self.video_frame_size
233
- filepath_ph_vid = os.path.join(self.output_dir, filename_ph)
234
  text_clip_object_ph = None
235
  try:
236
- text_clip_object_ph = TextClip(text_desc_ph, fontsize=50, color='white', font=self.video_overlay_font,
237
- bg_color='black', size=size_ph, method='caption').set_duration(duration_ph)
238
- text_clip_object_ph.write_videofile(filepath_ph_vid, fps=24, codec='libx264', preset='ultrafast', logger=None, threads=2)
239
- logger.info(f"Generic placeholder video created: {filepath_ph_vid}")
240
- return filepath_ph_vid
241
  except Exception as e_placeholder_video_creation:
242
- logger.error(f"Failed to create generic placeholder video '{filepath_ph_vid}': {e_placeholder_video_creation}", exc_info=True)
243
  return None
244
  finally:
245
  if text_clip_object_ph and hasattr(text_clip_object_ph, 'close'):
@@ -249,6 +285,7 @@ class VisualEngine:
249
  def generate_scene_asset(self, image_generation_prompt_text, motion_prompt_text_for_video,
250
  scene_data_dict, scene_identifier_fn_base,
251
  generate_as_video_clip_flag=False, runway_target_dur_val=5):
 
252
  base_name_current_asset, _ = os.path.splitext(scene_identifier_fn_base)
253
  asset_info_return_obj = {'path': None, 'type': 'none', 'error': True, 'prompt_used': image_generation_prompt_text, 'error_message': 'Asset generation init failed'}
254
  path_to_input_image_for_runway = None
@@ -286,33 +323,41 @@ class VisualEngine:
286
  else:logger.warning("RunwayML selected but disabled. Use base img.");asset_info_return_obj['error']=True;asset_info_return_obj['error_message']=(asset_info_return_obj.get('error_message',"Base img ok.")+" RunwayML disabled; use base img.").strip();asset_info_return_obj['path']=path_to_input_image_for_runway;asset_info_return_obj['type']='image';asset_info_return_obj['prompt_used']=image_generation_prompt_text
287
  return asset_info_return_obj
288
 
289
- def generate_narration_audio(self, narration_text, output_fn="narration_overall.mp3"):
290
- if not self.USE_ELEVENLABS or not self.elevenlabs_client_instance or not narration_text: logger.info("11L conditions not met. Skip audio."); return None
291
- narration_fp = os.path.join(self.output_dir, output_fn)
292
- try:
293
- logger.info(f"11L audio (Voice:{self.elevenlabs_voice_id}): \"{narration_text[:70]}...\"")
294
- stream_method = None
295
- if hasattr(self.elevenlabs_client_instance,'text_to_speech') and hasattr(self.elevenlabs_client_instance.text_to_speech,'stream'): stream_method=self.elevenlabs_client_instance.text_to_speech.stream; logger.info("Using 11L .text_to_speech.stream()")
296
- elif hasattr(self.elevenlabs_client_instance,'generate_stream'): stream_method=self.elevenlabs_client_instance.generate_stream; logger.info("Using 11L .generate_stream()")
297
- elif hasattr(self.elevenlabs_client_instance,'generate'):
298
- logger.info("Using 11L .generate() (non-streaming).")
299
- voice_p = Voice(voice_id=str(self.elevenlabs_voice_id),settings=self.elevenlabs_voice_settings_obj) if Voice and self.elevenlabs_voice_settings_obj else str(self.elevenlabs_voice_id)
300
- audio_b = self.elevenlabs_client_instance.generate(text=narration_text,voice=voice_p,model="eleven_multilingual_v2")
301
- with open(narration_fp,"wb") as f_audio: f_audio.write(audio_b); logger.info(f"11L audio (non-stream): {narration_fp}"); return narration_fp
302
- else: logger.error("No recognized 11L audio method."); return None
303
- if stream_method:
304
- voice_stream_params={"voice_id":str(self.elevenlabs_voice_id)}
 
 
 
 
 
 
 
 
305
  if self.elevenlabs_voice_settings_obj:
306
- if hasattr(self.elevenlabs_voice_settings_obj,'model_dump'): voice_stream_params["voice_settings"]=self.elevenlabs_voice_settings_obj.model_dump()
307
- elif hasattr(self.elevenlabs_voice_settings_obj,'dict'): voice_stream_params["voice_settings"]=self.elevenlabs_voice_settings_obj.dict()
308
- else: voice_stream_params["voice_settings"]=self.elevenlabs_voice_settings_obj
309
- audio_iter = stream_method(text=narration_text,model_id="eleven_multilingual_v2",**voice_stream_params)
310
- with open(narration_fp,"wb") as f_audio_stream:
311
- for chunk_item in audio_iter:
312
- if chunk_item: f_audio_stream.write(chunk_item)
313
- logger.info(f"11L audio (stream): {narration_fp}"); return narration_fp
314
- except AttributeError as e_11l_attr: logger.error(f"11L SDK AttrError: {e_11l_attr}. SDK/methods changed?", exc_info=True); return None
315
- except Exception as e_11l_gen: logger.error(f"11L audio gen error: {e_11l_gen}", exc_info=True); return None
316
 
317
  def assemble_animatic_from_assets(self, asset_data_list, overall_narration_path=None, output_filename="final_video.mp4", fps=24):
318
  if not asset_data_list: logger.warning("No assets for animatic."); return None
@@ -340,7 +385,7 @@ class VisualEngine:
340
  logger.debug(f"S{num_of_scene} (4-ToRGB): Final RGB. Mode:{final_rgb_image_for_pil.mode}, Size:{final_rgb_image_for_pil.size}")
341
  debug_path_img_pre_numpy = os.path.join(self.output_dir,f"debug_4_PRE_NUMPY_RGB_S{num_of_scene}.png"); final_rgb_image_for_pil.save(debug_path_img_pre_numpy); logger.info(f"CRITICAL DEBUG: Saved PRE_NUMPY_RGB_S{num_of_scene} to {debug_path_img_pre_numpy}")
342
 
343
- numpy_frame_arr = np.array(final_rgb_image_for_pil, dtype=np.uint8) # Explicit dtype
344
  if not numpy_frame_arr.flags['C_CONTIGUOUS']: numpy_frame_arr = np.ascontiguousarray(numpy_frame_arr, dtype=np.uint8)
345
  logger.debug(f"S{num_of_scene} (5-NumPy): Final NumPy. Shape:{numpy_frame_arr.shape}, DType:{numpy_frame_arr.dtype}, Flags:{numpy_frame_arr.flags}")
346
  if numpy_frame_arr.size == 0 or numpy_frame_arr.ndim != 3 or numpy_frame_arr.shape[2] != 3: logger.error(f"S{num_of_scene}: Invalid NumPy shape/size ({numpy_frame_arr.shape}). Skipping."); continue
@@ -349,7 +394,7 @@ class VisualEngine:
349
  logger.debug(f"S{num_of_scene} (6-ImageClip): Base ImageClip. Duration: {base_image_clip_mvpy.duration}")
350
 
351
  debug_path_moviepy_frame = os.path.join(self.output_dir,f"debug_7_MOVIEPY_FRAME_S{num_of_scene}.png")
352
- # <<< CORRECTED TRY-EXCEPT FOR save_frame >>>
353
  try:
354
  save_frame_time = min(0.1, base_image_clip_mvpy.duration / 2 if base_image_clip_mvpy.duration > 0 else 0.1)
355
  base_image_clip_mvpy.save_frame(debug_path_moviepy_frame, t=save_frame_time)
@@ -358,14 +403,13 @@ class VisualEngine:
358
  logger.error(f"DEBUG: Error saving frame FROM MOVIEPY ImageClip S{num_of_scene}: {e_save_mvpy_frame}", exc_info=True)
359
 
360
  fx_image_clip_mvpy = base_image_clip_mvpy
361
- try: # Ken Burns
362
  scale_end_kb_val = random.uniform(1.03, 1.08)
363
  if duration_for_scene > 0: fx_image_clip_mvpy = base_image_clip_mvpy.fx(vfx.resize, lambda t_val: 1 + (scale_end_kb_val - 1) * (t_val / duration_for_scene)).set_position('center'); logger.debug(f"S{num_of_scene} (8-KenBurns): Ken Burns applied.")
364
  else: logger.warning(f"S{num_of_scene}: Duration zero, skipping Ken Burns.")
365
  except Exception as e_kb_fx_loop: logger.error(f"S{num_of_scene} Ken Burns error: {e_kb_fx_loop}", exc_info=False)
366
  active_scene_clip = fx_image_clip_mvpy
367
  elif type_of_asset == 'video':
368
- # (Video processing logic as before)
369
  source_video_clip_obj=None
370
  try:
371
  logger.debug(f"S{num_of_scene}: Loading VIDEO asset: {path_of_asset}")
@@ -385,8 +429,7 @@ class VisualEngine:
385
  try: source_video_clip_obj.close()
386
  except Exception as e_close_src_vid: logger.warning(f"S{num_of_scene}: Error closing source VideoFileClip: {e_close_src_vid}")
387
  else: logger.warning(f"S{num_of_scene} Unknown asset type '{type_of_asset}'. Skipping."); continue
388
-
389
- if active_scene_clip and action_in_key: # Text Overlay
390
  try:
391
  dur_text_overlay_val=min(active_scene_clip.duration-0.5,active_scene_clip.duration*0.8)if active_scene_clip.duration>0.5 else (active_scene_clip.duration if active_scene_clip.duration > 0 else 0)
392
  start_text_overlay_val=0.25 if active_scene_clip.duration > 0.5 else 0
@@ -396,7 +439,6 @@ class VisualEngine:
396
  logger.debug(f"S{num_of_scene}: Text overlay composited.")
397
  else: logger.warning(f"S{num_of_scene}: Text overlay duration zero or negative ({dur_text_overlay_val}). Skipping text overlay.")
398
  except Exception as e_txt_comp_loop:logger.error(f"S{num_of_scene} TextClip compositing error:{e_txt_comp_loop}. Proceeding without text for this scene.",exc_info=True)
399
-
400
  if active_scene_clip: processed_moviepy_clips_list.append(active_scene_clip); logger.info(f"S{num_of_scene}: Asset successfully processed. Clip duration: {active_scene_clip.duration:.2f}s. Added to final list.")
401
  except Exception as e_asset_loop_main_exc: logger.error(f"MAJOR UNHANDLED ERROR processing asset for S{num_of_scene} (Path: {path_of_asset}): {e_asset_loop_main_exc}", exc_info=True)
402
  finally:
 
4
  import mimetypes
5
  import numpy as np
6
  import os
7
+ import openai # OpenAI v1.x.x+
8
  import requests
9
  import io
10
  import time
 
15
  CompositeVideoClip, AudioFileClip)
16
  import moviepy.video.fx.all as vfx
17
 
18
+ try: # MONKEY PATCH for Pillow/MoviePy compatibility
19
+ if hasattr(Image, 'Resampling') and hasattr(Image.Resampling, 'LANCZOS'): # Pillow 9+
20
  if not hasattr(Image, 'ANTIALIAS'): Image.ANTIALIAS = Image.Resampling.LANCZOS
21
+ elif hasattr(Image, 'LANCZOS'): # Pillow 8
22
  if not hasattr(Image, 'ANTIALIAS'): Image.ANTIALIAS = Image.LANCZOS
23
  elif not hasattr(Image, 'ANTIALIAS'):
24
  print("WARNING: Pillow version lacks common Resampling or ANTIALIAS. MoviePy effects might fail.")
25
  except Exception as e_mp: print(f"WARNING: ANTIALIAS monkey-patch error: {e_mp}")
26
 
27
  logger = logging.getLogger(__name__)
28
+ # logger.setLevel(logging.DEBUG) # Uncomment for maximum verbosity during active debugging
29
 
30
  ELEVENLABS_CLIENT_IMPORTED = False; ElevenLabsAPIClient = None; Voice = None; VoiceSettings = None
31
  try:
 
42
  logger.info("RunwayML SDK imported.")
43
  except Exception as e_rwy_imp: logger.warning(f"RunwayML SDK import failed: {e_rwy_imp}. RunwayML disabled.")
44
 
45
+
46
  class VisualEngine:
47
  DEFAULT_FONT_SIZE_PIL = 10; PREFERRED_FONT_SIZE_PIL = 20
48
  VIDEO_OVERLAY_FONT_SIZE = 30; VIDEO_OVERLAY_FONT_COLOR = 'white'
49
  DEFAULT_MOVIEPY_FONT = 'DejaVu-Sans-Bold'; PREFERRED_MOVIEPY_FONT = 'Liberation-Sans-Bold'
50
 
51
+ # <<< CRITICAL __init__ METHOD - ENSURE IT MATCHES THIS >>>
52
  def __init__(self, output_dir="temp_cinegen_media", default_elevenlabs_voice_id="Rachel"):
53
  self.output_dir = output_dir
54
  try:
55
  os.makedirs(self.output_dir, exist_ok=True)
56
  logger.info(f"VisualEngine output directory set/ensured: {os.path.abspath(self.output_dir)}")
57
+ # Test writability immediately
58
+ test_file_path = os.path.join(self.output_dir, ".ve_write_test.txt")
59
+ with open(test_file_path, "w") as f_test:
60
+ f_test.write("VisualEngine write test OK")
61
+ os.remove(test_file_path)
62
+ logger.info(f"Write test to output directory '{self.output_dir}' successful.")
63
+ except Exception as e_mkdir_init: # More specific exception catching
64
+ logger.critical(f"CRITICAL FAILURE: Could not create or write to output directory '{os.path.abspath(self.output_dir)}': {e_mkdir_init}", exc_info=True)
65
+ raise OSError(f"VisualEngine failed to initialize output directory '{self.output_dir}'. Check permissions and path.") from e_mkdir_init
66
 
67
  self.font_filename_pil_preference = "DejaVuSans-Bold.ttf"
68
+ font_paths_to_try = [ self.font_filename_pil_preference, f"/usr/share/fonts/truetype/dejavu/{self.font_filename_pil_preference}", f"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", f"/System/Library/Fonts/Supplemental/Arial.ttf", f"C:/Windows/Fonts/arial.ttf", f"/usr/local/share/fonts/truetype/mycustomfonts/arial.ttf"]
69
+ self.resolved_font_path_pil = next((p for p in font_paths_to_try if os.path.exists(p)), None)
70
+
71
+ self.active_font_pil = ImageFont.load_default()
72
+ self.active_font_size_pil = self.DEFAULT_FONT_SIZE_PIL
73
+ self.active_moviepy_font_name = self.DEFAULT_MOVIEPY_FONT
74
+
75
  if self.resolved_font_path_pil:
76
  try:
77
  self.active_font_pil = ImageFont.truetype(self.resolved_font_path_pil, self.PREFERRED_FONT_SIZE_PIL)
78
  self.active_font_size_pil = self.PREFERRED_FONT_SIZE_PIL
79
  logger.info(f"Pillow font loaded: {self.resolved_font_path_pil} at size {self.active_font_size_pil}.")
80
  self.active_moviepy_font_name = 'DejaVu-Sans-Bold' if "dejavu" in self.resolved_font_path_pil.lower() else ('Liberation-Sans-Bold' if "liberation" in self.resolved_font_path_pil.lower() else self.DEFAULT_MOVIEPY_FONT)
81
+ except IOError as e_font_load: logger.error(f"Pillow font IOError for '{self.resolved_font_path_pil}': {e_font_load}. Using default.")
82
+ else: logger.warning("Preferred Pillow font not found in predefined paths. Using default.")
83
 
84
  self.openai_api_key = None; self.USE_AI_IMAGE_GENERATION = False; self.dalle_model = "dall-e-3"; self.image_size_dalle3 = "1792x1024"
85
  self.video_frame_size = (1280, 720)
86
+
87
  self.elevenlabs_api_key = None; self.USE_ELEVENLABS = False; self.elevenlabs_client_instance = None
88
+ self.elevenlabs_voice_id = default_elevenlabs_voice_id
89
  logger.info(f"VisualEngine __init__: ElevenLabs Voice ID initially set to: {self.elevenlabs_voice_id}")
90
+
91
  if VoiceSettings and ELEVENLABS_CLIENT_IMPORTED: self.elevenlabs_voice_settings_obj = VoiceSettings(stability=0.60, similarity_boost=0.80, style=0.15, use_speaker_boost=True)
92
  else: self.elevenlabs_voice_settings_obj = None
93
+
94
  self.pexels_api_key = None; self.USE_PEXELS = False
95
  self.runway_api_key = None; self.USE_RUNWAYML = False; self.runway_ml_sdk_client_instance = None
96
+
97
  if RUNWAYML_SDK_IMPORTED and RunwayMLAPIClientClass and os.getenv("RUNWAYML_API_SECRET"):
98
+ try: self.runway_ml_sdk_client_instance = RunwayMLAPIClientClass(); self.USE_RUNWAYML = True; logger.info("RunwayML Client initialized using RUNWAYML_API_SECRET env var at startup.")
99
+ except Exception as e_rwy_init_constructor: logger.error(f"Initial RunwayML client initialization via env var failed: {e_rwy_init_constructor}"); self.USE_RUNWAYML = False
100
+
101
+ logger.info("VisualEngine __init__ sequence fully completed.")
102
 
103
  def set_openai_api_key(self, api_key_value): self.openai_api_key = api_key_value; self.USE_AI_IMAGE_GENERATION = bool(api_key_value); logger.info(f"DALL-E status: {'Ready' if self.USE_AI_IMAGE_GENERATION else 'Disabled'}")
104
+
105
  def set_elevenlabs_api_key(self, api_key_value, voice_id_from_secret=None):
106
  self.elevenlabs_api_key = api_key_value
107
  if voice_id_from_secret: self.elevenlabs_voice_id = voice_id_from_secret; logger.info(f"11L Voice ID updated via set_elevenlabs_api_key to: {self.elevenlabs_voice_id}")
108
+
109
  if api_key_value and ELEVENLABS_CLIENT_IMPORTED and ElevenLabsAPIClient:
110
+ try: self.elevenlabs_client_instance = ElevenLabsAPIClient(api_key=api_key_value); self.USE_ELEVENLABS = bool(self.elevenlabs_client_instance); logger.info(f"11L Client: {'Ready' if self.USE_ELEVENLABS else 'Failed Initialization'} (Using Voice: {self.elevenlabs_voice_id})")
111
+ except Exception as e_11l_setkey_init: logger.error(f"11L client initialization error: {e_11l_setkey_init}. Service Disabled.", exc_info=True); self.USE_ELEVENLABS=False; self.elevenlabs_client_instance=None
112
+ else: self.USE_ELEVENLABS = False; logger.info(f"11L Service Disabled (API key not provided or SDK component issue).")
113
+
114
  def set_pexels_api_key(self, api_key_value): self.pexels_api_key = api_key_value; self.USE_PEXELS = bool(api_key_value); logger.info(f"Pexels status: {'Ready' if self.USE_PEXELS else 'Disabled'}")
115
+
116
  def set_runway_api_key(self, api_key_value):
117
  self.runway_api_key = api_key_value
118
  if api_key_value:
119
  if RUNWAYML_SDK_IMPORTED and RunwayMLAPIClientClass:
120
+ if not self.runway_ml_sdk_client_instance: # If not already initialized by env var
121
  try:
122
+ original_env_secret_val = os.getenv("RUNWAYML_API_SECRET") # Renamed
123
+ if not original_env_secret_val: os.environ["RUNWAYML_API_SECRET"] = api_key_value; logger.info("Temporarily set RUNWAYML_API_SECRET from provided key for SDK client init.")
124
  self.runway_ml_sdk_client_instance = RunwayMLAPIClientClass(); self.USE_RUNWAYML = True; logger.info("RunwayML Client initialized successfully via set_runway_api_key.")
125
+ if not original_env_secret_val: del os.environ["RUNWAYML_API_SECRET"]; logger.info("Cleared temporary RUNWAYML_API_SECRET environment variable.")
126
+ except Exception as e_runway_setkey_init_local: logger.error(f"RunwayML Client initialization in set_runway_api_key failed: {e_runway_setkey_init_local}", exc_info=True); self.USE_RUNWAYML=False;self.runway_ml_sdk_client_instance=None # Renamed
127
  else: self.USE_RUNWAYML = True; logger.info("RunwayML Client was already initialized (likely from environment variable). API key stored.")
128
  else: logger.warning("RunwayML SDK not imported. API key stored, but current integration relies on SDK. Service effectively disabled."); self.USE_RUNWAYML = False
129
  else: self.USE_RUNWAYML = False; self.runway_ml_sdk_client_instance = None; logger.info("RunwayML Service Disabled (no API key provided).")
130
 
131
+ # --- Helper Methods (_image_to_data_uri, _map_resolution_to_runway_ratio, etc.) ---
132
+ # (These should be the corrected versions from previous iterations)
133
+ def _image_to_data_uri(self, image_path_in): # Renamed image_path
134
  try:
135
+ mime_type_val, _ = mimetypes.guess_type(image_path_in) # Renamed
136
+ if not mime_type_val:
137
+ file_ext = os.path.splitext(image_path_in)[1].lower() # Renamed
138
+ mime_type_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"} # Renamed
139
+ mime_type_val = mime_type_map.get(file_ext, "application/octet-stream")
140
+ if mime_type_val == "application/octet-stream": logger.warning(f"Could not determine MIME type for {image_path_in} from ext '{file_ext}', using default {mime_type_val}.")
141
+ with open(image_path_in, "rb") as img_file_handle: img_binary_data = img_file_handle.read() # Renamed
142
+ encoded_b64_str = base64.b64encode(img_binary_data).decode('utf-8') # Renamed
143
+ final_data_uri = f"data:{mime_type_val};base64,{encoded_b64_str}"; logger.debug(f"Data URI for {os.path.basename(image_path_in)} (MIME:{mime_type_val}): {final_data_uri[:100]}..."); return final_data_uri # Renamed
144
+ except FileNotFoundError: logger.error(f"Img not found {image_path_in} for data URI."); return None
145
+ except Exception as e_to_data_uri: logger.error(f"Error converting {image_path_in} to data URI:{e_to_data_uri}", exc_info=True); return None # Renamed
146
 
147
+ def _map_resolution_to_runway_ratio(self, width_in, height_in): # Renamed
148
+ ratio_string = f"{width_in}:{height_in}"; supported_ratios = ["1280:720","720:1280","1104:832","832:1104","960:960","1584:672"]; # Renamed
149
+ if ratio_string in supported_ratios: return ratio_string
150
+ logger.warning(f"Res {ratio_string} not in Gen-4 list. Default 1280:720 for Runway."); return "1280:720"
151
 
152
+ def _get_text_dimensions(self, text_str, font_pil_obj): # Renamed
153
+ def_h = getattr(font_pil_obj, 'size', self.active_font_size_pil);
154
+ if not text_str: return 0, def_h
155
  try:
156
+ if hasattr(font_pil_obj,'getbbox'): box = font_pil_obj.getbbox(text_str); w_val=box[2]-box[0]; h_val=box[3]-box[1]; return w_val, h_val if h_val > 0 else def_h # Renamed
157
+ elif hasattr(font_pil_obj,'getsize'): w_val,h_val=font_pil_obj.getsize(text_str); return w_val, h_val if h_val > 0 else def_h # Renamed
158
+ else: return int(len(text_str)*def_h*0.6), int(def_h*1.2)
159
+ except Exception as e_get_dim: logger.warning(f"Error in _get_text_dimensions: {e_get_dim}"); return int(len(text_str)*self.active_font_size_pil*0.6),int(self.active_font_size_pil*1.2) # Renamed
160
 
161
+ def _create_placeholder_image_content(self,text_desc_val, filename_val, size_val=None): # Renamed
162
+ # (Corrected version from previous responses)
163
+ if size_val is None: size_val = self.video_frame_size
164
+ placeholder_img = Image.new('RGB', size_val, color=(20, 20, 40)); placeholder_draw = ImageDraw.Draw(placeholder_img); ph_padding = 25 # Renamed
165
+ ph_max_w = size_val[0] - (2 * ph_padding); ph_lines = []
166
+ if not text_desc_val: text_desc_val = "(Placeholder Image)"
167
+ ph_words = text_desc_val.split(); ph_current_line = ""
168
+ for ph_word_idx, ph_word in enumerate(ph_words):
169
+ ph_prosp_add = ph_word + (" " if ph_word_idx < len(ph_words) - 1 else "")
170
+ ph_test_line = ph_current_line + ph_prosp_add
171
+ ph_curr_w, _ = self._get_text_dimensions(ph_test_line, self.active_font_pil)
172
+ if ph_curr_w == 0 and ph_test_line.strip(): ph_curr_w = len(ph_test_line) * (self.active_font_size_pil * 0.6)
173
+ if ph_curr_w <= ph_max_w: ph_current_line = ph_test_line
174
  else:
175
+ if ph_current_line.strip(): ph_lines.append(ph_current_line.strip())
176
+ ph_current_line = ph_prosp_add
177
+ if ph_current_line.strip(): ph_lines.append(ph_current_line.strip())
178
+ if not ph_lines and text_desc_val:
179
+ ph_avg_char_w, _ = self._get_text_dimensions("W", self.active_font_pil); ph_avg_char_w = ph_avg_char_w or (self.active_font_size_pil * 0.6)
180
+ ph_chars_line = int(ph_max_w / ph_avg_char_w) if ph_avg_char_w > 0 else 20
181
+ ph_lines.append(text_desc_val[:ph_chars_line] + ("..." if len(text_desc_val) > ph_chars_line else ""))
182
+ elif not ph_lines: ph_lines.append("(Placeholder Error)")
183
+ _, ph_single_h = self._get_text_dimensions("Ay", self.active_font_pil); ph_single_h = ph_single_h if ph_single_h > 0 else self.active_font_size_pil + 2
184
+ ph_max_l = min(len(ph_lines), (size_val[1] - (2 * ph_padding)) // (ph_single_h + 2)) if ph_single_h > 0 else 1; ph_max_l = max(1, ph_max_l)
185
+ ph_y_pos = ph_padding + (size_val[1] - (2 * ph_padding) - ph_max_l * (ph_single_h + 2)) / 2.0
186
+ for ph_i_line in range(ph_max_l):
187
+ ph_line_txt = ph_lines[ph_i_line]; ph_line_w, _ = self._get_text_dimensions(ph_line_txt, self.active_font_pil)
188
+ if ph_line_w == 0 and ph_line_txt.strip(): ph_line_w = len(ph_line_txt) * (self.active_font_size_pil * 0.6)
189
+ ph_x_pos = (size_val[0] - ph_line_w) / 2.0
190
+ try: placeholder_draw.text((ph_x_pos, ph_y_pos), ph_line_txt, font=self.active_font_pil, fill=(200, 200, 180))
191
+ except Exception as e_ph_draw: logger.error(f"Pillow d.text error: {e_ph_draw} for '{ph_line_txt}'")
192
+ ph_y_pos += ph_single_h + 2
193
+ if ph_i_line == 6 and ph_max_l > 7:
194
+ try: placeholder_draw.text((ph_x_pos, ph_y_pos), "...", font=self.active_font_pil, fill=(200, 200, 180))
195
+ except Exception as e_ph_elip: logger.error(f"Pillow d.text ellipsis error: {e_ph_elip}"); break
196
+ ph_filepath = os.path.join(self.output_dir, filename_val)
197
+ try: placeholder_img.save(ph_filepath); return ph_filepath
198
+ except Exception as e_ph_save: logger.error(f"Saving placeholder image '{ph_filepath}' error: {e_ph_save}", exc_info=True); return None
199
 
200
+ def _search_pexels_image(self, pexels_query, pexels_output_fn_base): # Renamed
201
  if not self.USE_PEXELS or not self.pexels_api_key: return None
202
+ pexels_headers = {"Authorization": self.pexels_api_key}
203
+ pexels_params = {"query": pexels_query, "per_page": 1, "orientation": "landscape", "size": "large2x"}
204
+ pexels_base_name, _ = os.path.splitext(pexels_output_fn_base)
205
+ pexels_output_filename = pexels_base_name + f"_pexels_{random.randint(1000,9999)}.jpg"
206
+ pexels_filepath = os.path.join(self.output_dir, pexels_output_filename)
207
  try:
208
+ logger.info(f"Pexels: Searching for '{pexels_query}'")
209
+ pexels_eff_query = " ".join(pexels_query.split()[:5])
210
+ pexels_params["query"] = pexels_eff_query
211
+ pexels_response = requests.get("https://api.pexels.com/v1/search", headers=pexels_headers, params=pexels_params, timeout=20)
212
+ pexels_response.raise_for_status()
213
+ pexels_data = pexels_response.json()
214
+ if pexels_data.get("photos") and len(pexels_data["photos"]) > 0:
215
+ pexels_photo_details = pexels_data["photos"][0]
216
+ pexels_photo_url = pexels_photo_details.get("src", {}).get("large2x")
217
+ if not pexels_photo_url: logger.warning(f"Pexels: 'large2x' URL missing for '{pexels_eff_query}'. Details: {pexels_photo_details}"); return None
218
+ pexels_image_response = requests.get(pexels_photo_url, timeout=60); pexels_image_response.raise_for_status()
219
+ pexels_img_pil_data = Image.open(io.BytesIO(pexels_image_response.content))
220
+ if pexels_img_pil_data.mode != 'RGB': pexels_img_pil_data = pexels_img_pil_data.convert('RGB')
221
+ pexels_img_pil_data.save(pexels_filepath); logger.info(f"Pexels: Image saved to {pexels_filepath}"); return pexels_filepath
222
+ else: logger.info(f"Pexels: No photos for '{pexels_eff_query}'."); return None
223
+ except requests.exceptions.RequestException as e_pexels_req: logger.error(f"Pexels: RequestException for '{pexels_query}': {e_pexels_req}", exc_info=False); return None
224
+ except Exception as e_pexels_general: logger.error(f"Pexels: General error for '{pexels_query}': {e_pexels_general}", exc_info=True); return None
225
+
226
+ # ... (Rest of methods: _generate_video_clip_with_runwayml, generate_scene_asset, generate_narration_audio, assemble_animatic_from_assets)
227
+ # Ensure these are taken from the last fully corrected versions provided, paying close attention to their specific fixes.
228
+ # For example, generate_narration_audio had its own try-except fix.
229
+ # assemble_animatic_from_assets had extensive debugging for image corruption.
230
+
231
+ # For brevity, I will paste the corrected generate_narration_audio and the
232
+ # structure for generate_scene_asset and assemble_animatic_from_assets.
233
+ # You MUST ensure the internal logic of generate_scene_asset and assemble_animatic_from_assets
234
+ # matches the last "expert" versions that included detailed debugging for image/video issues.
235
 
236
  def _generate_video_clip_with_runwayml(self, motion_prompt_rwy, input_img_path_rwy, scene_id_base_fn_rwy, duration_s_rwy=5):
237
+ # (Keep robust RunwayML logic from before, with proper SDK client instance: self.runway_ml_sdk_client_instance)
238
  if not self.USE_RUNWAYML or not self.runway_ml_sdk_client_instance: logger.warning("RunwayML skip: Not enabled/client not init."); return None
239
  if not input_img_path_rwy or not os.path.exists(input_img_path_rwy): logger.error(f"Runway Gen-4 needs input img. Invalid: {input_img_path_rwy}"); return None
240
  img_data_uri_rwy = self._image_to_data_uri(input_img_path_rwy)
 
264
  except AttributeError as e_rwy_sdk_attr: logger.error(f"RunwayML SDK AttrError:{e_rwy_sdk_attr}. SDK methods changed?",exc_info=True);return None
265
  except Exception as e_rwy_general: logger.error(f"Runway Gen-4 API error:{e_rwy_general}",exc_info=True);return None
266
 
267
+ def _create_placeholder_video_content(self, text_desc_ph_vid, filename_ph_vid, duration_ph_vid=4, size_ph_vid=None):
268
+ if size_ph_vid is None: size_ph_vid = self.video_frame_size
269
+ filepath_ph_vid_out = os.path.join(self.output_dir, filename_ph_vid)
270
  text_clip_object_ph = None
271
  try:
272
+ text_clip_object_ph = TextClip(text_desc_ph_vid, fontsize=50, color='white', font=self.video_overlay_font,
273
+ bg_color='black', size=size_ph_vid, method='caption').set_duration(duration_ph_vid)
274
+ text_clip_object_ph.write_videofile(filepath_ph_vid_out, fps=24, codec='libx264', preset='ultrafast', logger=None, threads=2)
275
+ logger.info(f"Generic placeholder video created: {filepath_ph_vid_out}")
276
+ return filepath_ph_vid_out
277
  except Exception as e_placeholder_video_creation:
278
+ logger.error(f"Failed to create generic placeholder video '{filepath_ph_vid_out}': {e_placeholder_video_creation}", exc_info=True)
279
  return None
280
  finally:
281
  if text_clip_object_ph and hasattr(text_clip_object_ph, 'close'):
 
285
  def generate_scene_asset(self, image_generation_prompt_text, motion_prompt_text_for_video,
286
  scene_data_dict, scene_identifier_fn_base,
287
  generate_as_video_clip_flag=False, runway_target_dur_val=5):
288
+ # (Corrected DALL-E loop from previous response)
289
  base_name_current_asset, _ = os.path.splitext(scene_identifier_fn_base)
290
  asset_info_return_obj = {'path': None, 'type': 'none', 'error': True, 'prompt_used': image_generation_prompt_text, 'error_message': 'Asset generation init failed'}
291
  path_to_input_image_for_runway = None
 
323
  else:logger.warning("RunwayML selected but disabled. Use base img.");asset_info_return_obj['error']=True;asset_info_return_obj['error_message']=(asset_info_return_obj.get('error_message',"Base img ok.")+" RunwayML disabled; use base img.").strip();asset_info_return_obj['path']=path_to_input_image_for_runway;asset_info_return_obj['type']='image';asset_info_return_obj['prompt_used']=image_generation_prompt_text
324
  return asset_info_return_obj
325
 
326
+ def generate_narration_audio(self, text_to_narrate, output_filename="narration_overall.mp3"):
327
+ # <<< CORRECTED METHOD with try/except >>>
328
+ if not self.USE_ELEVENLABS or not self.elevenlabs_client_instance or not text_to_narrate:
329
+ logger.info("ElevenLabs conditions not met (service disabled, client not init, or no text). Skipping audio generation.")
330
+ return None
331
+ audio_filepath_narration = os.path.join(self.output_dir, output_filename)
332
+ try: # Main try block for the entire operation
333
+ logger.info(f"Generating ElevenLabs audio (Voice ID: {self.elevenlabs_voice_id}) for text: \"{text_to_narrate[:70]}...\"")
334
+ audio_stream_method_11l = None
335
+ if hasattr(self.elevenlabs_client_instance, 'text_to_speech') and hasattr(self.elevenlabs_client_instance.text_to_speech, 'stream'):
336
+ audio_stream_method_11l = self.elevenlabs_client_instance.text_to_speech.stream; logger.info("Using ElevenLabs SDK method: client.text_to_speech.stream()")
337
+ elif hasattr(self.elevenlabs_client_instance, 'generate_stream'):
338
+ audio_stream_method_11l = self.elevenlabs_client_instance.generate_stream; logger.info("Using ElevenLabs SDK method: client.generate_stream()")
339
+ elif hasattr(self.elevenlabs_client_instance, 'generate'):
340
+ logger.info("Using ElevenLabs SDK method: client.generate() (non-streaming).")
341
+ voice_param_11l = str(self.elevenlabs_voice_id)
342
+ if Voice and self.elevenlabs_voice_settings_obj: voice_param_11l = Voice(voice_id=str(self.elevenlabs_voice_id), settings=self.elevenlabs_voice_settings_obj)
343
+ audio_bytes_data = self.elevenlabs_client_instance.generate(text=text_to_narrate, voice=voice_param_11l, model="eleven_multilingual_v2")
344
+ with open(audio_filepath_narration, "wb") as audio_file_out: audio_file_out.write(audio_bytes_data)
345
+ logger.info(f"ElevenLabs audio (non-streamed) saved successfully to: {audio_filepath_narration}"); return audio_filepath_narration
346
+ else: logger.error("No recognized audio generation method found on the ElevenLabs client instance."); return None
347
+
348
+ if audio_stream_method_11l:
349
+ params_for_voice_stream = {"voice_id": str(self.elevenlabs_voice_id)}
350
  if self.elevenlabs_voice_settings_obj:
351
+ if hasattr(self.elevenlabs_voice_settings_obj, 'model_dump'): params_for_voice_stream["voice_settings"] = self.elevenlabs_voice_settings_obj.model_dump()
352
+ elif hasattr(self.elevenlabs_voice_settings_obj, 'dict'): params_for_voice_stream["voice_settings"] = self.elevenlabs_voice_settings_obj.dict()
353
+ else: params_for_voice_stream["voice_settings"] = self.elevenlabs_voice_settings_obj
354
+ audio_data_iterator_11l = audio_stream_method_11l(text=text_to_narrate, model_id="eleven_multilingual_v2", **params_for_voice_stream)
355
+ with open(audio_filepath_narration, "wb") as audio_file_out_stream:
356
+ for audio_chunk_data in audio_data_iterator_11l:
357
+ if audio_chunk_data: audio_file_out_stream.write(audio_chunk_data)
358
+ logger.info(f"ElevenLabs audio (streamed) saved successfully to: {audio_filepath_narration}"); return audio_filepath_narration
359
+ except AttributeError as ae_11l_sdk: logger.error(f"AttributeError with ElevenLabs SDK client: {ae_11l_sdk}. SDK version/methods might differ.", exc_info=True); return None
360
+ except Exception as e_11l_general_audio: logger.error(f"General error during ElevenLabs audio generation: {e_11l_general_audio}", exc_info=True); return None
361
 
362
  def assemble_animatic_from_assets(self, asset_data_list, overall_narration_path=None, output_filename="final_video.mp4", fps=24):
363
  if not asset_data_list: logger.warning("No assets for animatic."); return None
 
385
  logger.debug(f"S{num_of_scene} (4-ToRGB): Final RGB. Mode:{final_rgb_image_for_pil.mode}, Size:{final_rgb_image_for_pil.size}")
386
  debug_path_img_pre_numpy = os.path.join(self.output_dir,f"debug_4_PRE_NUMPY_RGB_S{num_of_scene}.png"); final_rgb_image_for_pil.save(debug_path_img_pre_numpy); logger.info(f"CRITICAL DEBUG: Saved PRE_NUMPY_RGB_S{num_of_scene} to {debug_path_img_pre_numpy}")
387
 
388
+ numpy_frame_arr = np.array(final_rgb_image_for_pil, dtype=np.uint8)
389
  if not numpy_frame_arr.flags['C_CONTIGUOUS']: numpy_frame_arr = np.ascontiguousarray(numpy_frame_arr, dtype=np.uint8)
390
  logger.debug(f"S{num_of_scene} (5-NumPy): Final NumPy. Shape:{numpy_frame_arr.shape}, DType:{numpy_frame_arr.dtype}, Flags:{numpy_frame_arr.flags}")
391
  if numpy_frame_arr.size == 0 or numpy_frame_arr.ndim != 3 or numpy_frame_arr.shape[2] != 3: logger.error(f"S{num_of_scene}: Invalid NumPy shape/size ({numpy_frame_arr.shape}). Skipping."); continue
 
394
  logger.debug(f"S{num_of_scene} (6-ImageClip): Base ImageClip. Duration: {base_image_clip_mvpy.duration}")
395
 
396
  debug_path_moviepy_frame = os.path.join(self.output_dir,f"debug_7_MOVIEPY_FRAME_S{num_of_scene}.png")
397
+ # <<< THIS IS THE CORRECTED TRY-EXCEPT BLOCK >>>
398
  try:
399
  save_frame_time = min(0.1, base_image_clip_mvpy.duration / 2 if base_image_clip_mvpy.duration > 0 else 0.1)
400
  base_image_clip_mvpy.save_frame(debug_path_moviepy_frame, t=save_frame_time)
 
403
  logger.error(f"DEBUG: Error saving frame FROM MOVIEPY ImageClip S{num_of_scene}: {e_save_mvpy_frame}", exc_info=True)
404
 
405
  fx_image_clip_mvpy = base_image_clip_mvpy
406
+ try:
407
  scale_end_kb_val = random.uniform(1.03, 1.08)
408
  if duration_for_scene > 0: fx_image_clip_mvpy = base_image_clip_mvpy.fx(vfx.resize, lambda t_val: 1 + (scale_end_kb_val - 1) * (t_val / duration_for_scene)).set_position('center'); logger.debug(f"S{num_of_scene} (8-KenBurns): Ken Burns applied.")
409
  else: logger.warning(f"S{num_of_scene}: Duration zero, skipping Ken Burns.")
410
  except Exception as e_kb_fx_loop: logger.error(f"S{num_of_scene} Ken Burns error: {e_kb_fx_loop}", exc_info=False)
411
  active_scene_clip = fx_image_clip_mvpy
412
  elif type_of_asset == 'video':
 
413
  source_video_clip_obj=None
414
  try:
415
  logger.debug(f"S{num_of_scene}: Loading VIDEO asset: {path_of_asset}")
 
429
  try: source_video_clip_obj.close()
430
  except Exception as e_close_src_vid: logger.warning(f"S{num_of_scene}: Error closing source VideoFileClip: {e_close_src_vid}")
431
  else: logger.warning(f"S{num_of_scene} Unknown asset type '{type_of_asset}'. Skipping."); continue
432
+ if active_scene_clip and action_in_key:
 
433
  try:
434
  dur_text_overlay_val=min(active_scene_clip.duration-0.5,active_scene_clip.duration*0.8)if active_scene_clip.duration>0.5 else (active_scene_clip.duration if active_scene_clip.duration > 0 else 0)
435
  start_text_overlay_val=0.25 if active_scene_clip.duration > 0.5 else 0
 
439
  logger.debug(f"S{num_of_scene}: Text overlay composited.")
440
  else: logger.warning(f"S{num_of_scene}: Text overlay duration zero or negative ({dur_text_overlay_val}). Skipping text overlay.")
441
  except Exception as e_txt_comp_loop:logger.error(f"S{num_of_scene} TextClip compositing error:{e_txt_comp_loop}. Proceeding without text for this scene.",exc_info=True)
 
442
  if active_scene_clip: processed_moviepy_clips_list.append(active_scene_clip); logger.info(f"S{num_of_scene}: Asset successfully processed. Clip duration: {active_scene_clip.duration:.2f}s. Added to final list.")
443
  except Exception as e_asset_loop_main_exc: logger.error(f"MAJOR UNHANDLED ERROR processing asset for S{num_of_scene} (Path: {path_of_asset}): {e_asset_loop_main_exc}", exc_info=True)
444
  finally: