metaai-sdk 2.3.4__py3-none-any.whl → 2.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
metaai_api/main.py CHANGED
@@ -141,6 +141,7 @@ class MetaAI:
141
141
  media_ids: Optional[list] = None,
142
142
  attachment_metadata: Optional[Dict[str, Any]] = None,
143
143
  is_image_generation: bool = False,
144
+ orientation: Optional[str] = None,
144
145
  ) -> Union[Dict, Generator[Dict, None, None]]:
145
146
  """
146
147
  Sends a message to the Meta AI and returns the response.
@@ -154,6 +155,7 @@ class MetaAI:
154
155
  media_ids (list): List of media IDs from uploaded images to include in the prompt. Defaults to None.
155
156
  attachment_metadata (dict): Optional dict with 'file_size' (int) and 'mime_type' (str). Defaults to None.
156
157
  is_image_generation (bool): Whether this is for image generation (vs chat). Defaults to False.
158
+ orientation (str): Image orientation for generation. Valid values: "LANDSCAPE", "VERTICAL", "SQUARE". Defaults to "VERTICAL".
157
159
 
158
160
  Returns:
159
161
  dict: A dictionary containing the response message and sources.
@@ -167,7 +169,10 @@ class MetaAI:
167
169
  url = "https://graph.meta.ai/graphql?locale=user"
168
170
 
169
171
  else:
170
- auth_payload = {"fb_dtsg": self.cookies["fb_dtsg"]}
172
+ auth_payload = {
173
+ "fb_dtsg": self.cookies["fb_dtsg"],
174
+ "lsd": self.cookies.get("lsd", ""),
175
+ }
171
176
  url = "https://www.meta.ai/api/graphql/"
172
177
 
173
178
  if not self.external_conversation_id or new_conversation:
@@ -186,13 +191,15 @@ class MetaAI:
186
191
 
187
192
  # Generate offline threading IDs
188
193
  offline_threading_id = generate_offline_threading_id()
194
+ bot_offline_threading_id = str(int(offline_threading_id) + 1)
195
+ thread_session_id = str(uuid.uuid4())
189
196
 
190
197
  # Determine entrypoint based on context
191
198
  if images:
192
199
  # Video generation with images uses CHAT
193
200
  entrypoint = "KADABRA__CHAT__UNIFIED_INPUT_BAR"
194
- elif media_ids:
195
- # Any uploaded image use (chat analysis or image generation) uses DISCOVER
201
+ elif media_ids or orientation:
202
+ # Image generation with orientation OR uploaded images uses DISCOVER
196
203
  entrypoint = "KADABRA__DISCOVER__UNIFIED_INPUT_BAR"
197
204
  else:
198
205
  entrypoint = "ABRA__CHAT__TEXT"
@@ -200,42 +207,154 @@ class MetaAI:
200
207
  # Set friendly name based on entrypoint
201
208
  friendly_name = "useKadabraSendMessageMutation" if entrypoint.startswith("KADABRA") else "useAbraSendMessageMutation"
202
209
 
210
+ # Build variables dictionary
211
+ is_kadabra = entrypoint.startswith("KADABRA")
212
+
213
+ if is_kadabra:
214
+ # Full Kadabra variables for image generation
215
+ variables = {
216
+ "message": {"sensitive_string_value": message},
217
+ "externalConversationId": self.external_conversation_id,
218
+ "offlineThreadingId": offline_threading_id,
219
+ "threadSessionId": thread_session_id,
220
+ "isNewConversation": new_conversation or not self.offline_threading_id,
221
+ "suggestedPromptIndex": None,
222
+ "promptPrefix": None,
223
+ "entrypoint": entrypoint,
224
+ "attachments": [],
225
+ "attachmentsV2": attachments_v2,
226
+ "activeMediaSets": [],
227
+ "activeCardVersions": [],
228
+ "activeArtifactVersion": None,
229
+ "userUploadEditModeInput": None,
230
+ "reelComposeInput": None,
231
+ "qplJoinId": uuid.uuid4().hex[:17],
232
+ "sourceRemixPostId": None,
233
+ "gkPlannerOrReasoningEnabled": True,
234
+ "selectedModel": "BASIC_OPTION",
235
+ "conversationMode": None,
236
+ "selectedAgentType": "PLANNER",
237
+ "agentSettings": None,
238
+ "conversationStarterId": None,
239
+ "promptType": None,
240
+ "artifactRewriteOptions": None,
241
+ "imagineOperationRequest": None,
242
+ "imagineClientOptions": {"orientation": orientation.upper() if orientation else "VERTICAL"},
243
+ "spaceId": None,
244
+ "sparkSnapshotId": None,
245
+ "topicPageId": None,
246
+ "includeSpace": False,
247
+ "storybookId": None,
248
+ "messagePersistentInput": {
249
+ "attachment_size": attachment_metadata.get('file_size') if attachment_metadata else None,
250
+ "attachment_type": attachment_metadata.get('mime_type') if attachment_metadata else None,
251
+ "bot_message_offline_threading_id": bot_offline_threading_id,
252
+ "conversation_mode": None,
253
+ "external_conversation_id": self.external_conversation_id,
254
+ "is_new_conversation": new_conversation or not self.offline_threading_id,
255
+ "meta_ai_entry_point": entrypoint,
256
+ "offline_threading_id": offline_threading_id,
257
+ "prompt_id": None,
258
+ "prompt_session_id": thread_session_id,
259
+ },
260
+ "alakazam_enabled": True,
261
+ "skipInFlightMessageWithParams": None,
262
+ "__relay_internal__pv__KadabraSocialSearchEnabledrelayprovider": False,
263
+ "__relay_internal__pv__KadabraZeitgeistEnabledrelayprovider": False,
264
+ "__relay_internal__pv__alakazam_enabledrelayprovider": True,
265
+ "__relay_internal__pv__sp_kadabra_survey_invitationrelayprovider": True,
266
+ "__relay_internal__pv__enable_kadabra_partial_resultsrelayprovider": False,
267
+ "__relay_internal__pv__AbraArtifactsEnabledrelayprovider": True,
268
+ "__relay_internal__pv__KadabraMemoryEnabledrelayprovider": False,
269
+ "__relay_internal__pv__AbraPlannerEnabledrelayprovider": True,
270
+ "__relay_internal__pv__AbraWidgetsEnabledrelayprovider": False,
271
+ "__relay_internal__pv__KadabraDeepResearchEnabledrelayprovider": False,
272
+ "__relay_internal__pv__KadabraThinkHarderEnabledrelayprovider": False,
273
+ "__relay_internal__pv__KadabraVergeEnabledrelayprovider": False,
274
+ "__relay_internal__pv__KadabraSpacesEnabledrelayprovider": False,
275
+ "__relay_internal__pv__KadabraProductSearchEnabledrelayprovider": False,
276
+ "__relay_internal__pv__KadabraAreServiceEnabledrelayprovider": False,
277
+ "__relay_internal__pv__kadabra_render_reasoning_response_statesrelayprovider": True,
278
+ "__relay_internal__pv__kadabra_reasoning_cotrelayprovider": False,
279
+ "__relay_internal__pv__AbraSearchInlineReferencesEnabledrelayprovider": True,
280
+ "__relay_internal__pv__AbraComposedTextWidgetsrelayprovider": True,
281
+ "__relay_internal__pv__KadabraNewCitationsEnabledrelayprovider": True,
282
+ "__relay_internal__pv__WebPixelRatiorelayprovider": 1,
283
+ "__relay_internal__pv__KadabraVideoDeliveryRequestrelayprovider": {
284
+ "dash_manifest_requests": [{}],
285
+ "progressive_url_requests": [{"quality": "HD"}, {"quality": "SD"}]
286
+ },
287
+ "__relay_internal__pv__KadabraWidgetsRedesignEnabledrelayprovider": False,
288
+ "__relay_internal__pv__kadabra_enable_send_message_retryrelayprovider": True,
289
+ "__relay_internal__pv__KadabraEmailCalendarIntegrationrelayprovider": False,
290
+ "__relay_internal__pv__ClippyUIrelayprovider": False,
291
+ "__relay_internal__pv__kadabra_reels_connect_featuresrelayprovider": False,
292
+ "__relay_internal__pv__AbraBugNubrelayprovider": False,
293
+ "__relay_internal__pv__AbraRedteamingrelayprovider": False,
294
+ "__relay_internal__pv__AbraDebugDevOnlyrelayprovider": False,
295
+ "__relay_internal__pv__kadabra_enable_open_in_editor_message_actionrelayprovider": True,
296
+ "__relay_internal__pv__BloksDeviceContextrelayprovider": {"pixel_ratio": 1},
297
+ "__relay_internal__pv__AbraThreadsEnabledrelayprovider": False,
298
+ "__relay_internal__pv__kadabra_story_builder_enabledrelayprovider": False,
299
+ "__relay_internal__pv__kadabra_imagine_canvas_enable_dev_settingsrelayprovider": False,
300
+ "__relay_internal__pv__kadabra_create_media_deletionrelayprovider": False,
301
+ "__relay_internal__pv__kadabra_moodboardrelayprovider": False,
302
+ "__relay_internal__pv__AbraArtifactDragImagineFromConversationrelayprovider": True,
303
+ "__relay_internal__pv__kadabra_media_item_renderer_heightrelayprovider": 545,
304
+ "__relay_internal__pv__kadabra_media_item_renderer_widthrelayprovider": 620,
305
+ "__relay_internal__pv__AbraQPDocUploadNuxTriggerNamerelayprovider": "meta_dot_ai_abra_web_doc_upload_nux_tour",
306
+ "__relay_internal__pv__AbraSurfaceNuxIDrelayprovider": "12177",
307
+ "__relay_internal__pv__KadabraConversationRenamingrelayprovider": True,
308
+ "__relay_internal__pv__AbraIsLoggedOutrelayprovider": not self.is_authed,
309
+ "__relay_internal__pv__KadabraCanvasDisplayHeaderV2relayprovider": False,
310
+ "__relay_internal__pv__AbraArtifactEditorDebugModerelayprovider": False,
311
+ "__relay_internal__pv__AbraArtifactEditorDownloadHTMLEnabledrelayprovider": False,
312
+ "__relay_internal__pv__kadabra_create_row_hover_optionsrelayprovider": False,
313
+ "__relay_internal__pv__kadabra_media_info_pillsrelayprovider": True,
314
+ "__relay_internal__pv__KadabraConcordInternalProfileBadgeEnabledrelayprovider": False,
315
+ "__relay_internal__pv__KadabraSocialGraphrelayprovider": True,
316
+ }
317
+ else:
318
+ # Simpler Abra variables for chat
319
+ variables = {
320
+ "message": {"sensitive_string_value": message},
321
+ "externalConversationId": self.external_conversation_id,
322
+ "offlineThreadingId": offline_threading_id,
323
+ "suggestedPromptIndex": None,
324
+ "flashVideoRecapInput": flash_video_input,
325
+ "flashPreviewInput": None,
326
+ "promptPrefix": None,
327
+ "entrypoint": entrypoint,
328
+ "attachments": [],
329
+ "attachmentsV2": attachments_v2,
330
+ "messagePersistentInput": {
331
+ "attachment_size": attachment_metadata.get('file_size') if attachment_metadata else None,
332
+ "attachment_type": attachment_metadata.get('mime_type') if attachment_metadata else None,
333
+ "external_conversation_id": self.external_conversation_id,
334
+ "offline_threading_id": offline_threading_id,
335
+ "meta_ai_entry_point": entrypoint,
336
+ } if media_ids else None,
337
+ "icebreaker_type": "TEXT",
338
+ "__relay_internal__pv__AbraDebugDevOnlyrelayprovider": False,
339
+ "__relay_internal__pv__WebPixelRatiorelayprovider": 1,
340
+ }
341
+
203
342
  payload = {
204
343
  **auth_payload,
205
344
  "fb_api_caller_class": "RelayModern",
206
345
  "fb_api_req_friendly_name": friendly_name,
207
- "variables": json.dumps(
208
- {
209
- "message": {"sensitive_string_value": message},
210
- "externalConversationId": self.external_conversation_id,
211
- "offlineThreadingId": offline_threading_id,
212
- "suggestedPromptIndex": None,
213
- "flashVideoRecapInput": flash_video_input,
214
- "flashPreviewInput": None,
215
- "promptPrefix": None,
216
- "entrypoint": entrypoint,
217
- "attachments": [],
218
- "attachmentsV2": attachments_v2,
219
- "messagePersistentInput": {
220
- "attachment_size": attachment_metadata.get('file_size') if attachment_metadata else None,
221
- "attachment_type": attachment_metadata.get('mime_type') if attachment_metadata else None,
222
- "external_conversation_id": self.external_conversation_id,
223
- "offline_threading_id": offline_threading_id,
224
- "meta_ai_entry_point": entrypoint,
225
- } if media_ids else None,
226
- "icebreaker_type": "TEXT",
227
- "__relay_internal__pv__AbraDebugDevOnlyrelayprovider": False,
228
- "__relay_internal__pv__WebPixelRatiorelayprovider": 1,
229
- }
230
- ),
346
+ "variables": json.dumps(variables),
231
347
  "server_timestamps": "true",
232
- "doc_id": "34429318783334028" if entrypoint.startswith("KADABRA") else "7783822248314888",
348
+ "doc_id": "24895882500088854" if is_kadabra else "7783822248314888",
233
349
  }
234
350
  payload = urllib.parse.urlencode(payload) # noqa
235
351
  headers = {
236
352
  "content-type": "application/x-www-form-urlencoded",
237
353
  "x-fb-friendly-name": friendly_name,
238
354
  }
355
+ # Add lsd header for authenticated requests
356
+ if self.cookies.get("lsd"):
357
+ headers["x-fb-lsd"] = self.cookies["lsd"]
239
358
  if self.is_authed:
240
359
  headers["cookie"] = f'abra_sess={self.cookies["abra_sess"]}'
241
360
  # Recreate the session to avoid cookie leakage when user is authenticated
@@ -248,7 +367,7 @@ class MetaAI:
248
367
  raw_response = response.text
249
368
  last_streamed_response = self.extract_last_response(raw_response)
250
369
  if not last_streamed_response:
251
- return self.retry(message, stream=stream, attempts=attempts, media_ids=media_ids, attachment_metadata=attachment_metadata, is_image_generation=is_image_generation)
370
+ return self.retry(message, stream=stream, attempts=attempts, new_conversation=new_conversation, images=images, media_ids=media_ids, attachment_metadata=attachment_metadata, is_image_generation=is_image_generation, orientation=orientation)
252
371
 
253
372
  extracted_data = self.extract_data(last_streamed_response)
254
373
  return extracted_data
@@ -257,10 +376,10 @@ class MetaAI:
257
376
  lines = response.iter_lines()
258
377
  is_error = json.loads(next(lines))
259
378
  if len(is_error.get("errors", [])) > 0:
260
- return self.retry(message, stream=stream, attempts=attempts, media_ids=media_ids, attachment_metadata=attachment_metadata, is_image_generation=is_image_generation)
379
+ return self.retry(message, stream=stream, attempts=attempts, new_conversation=new_conversation, images=images, media_ids=media_ids, attachment_metadata=attachment_metadata, is_image_generation=is_image_generation, orientation=orientation)
261
380
  return self.stream_response(lines)
262
381
 
263
- def retry(self, message: str, stream: bool = False, attempts: int = 0, media_ids: Optional[list] = None, attachment_metadata: Optional[Dict[str, Any]] = None, is_image_generation: bool = False):
382
+ def retry(self, message: str, stream: bool = False, attempts: int = 0, new_conversation: bool = False, images: Optional[list] = None, media_ids: Optional[list] = None, attachment_metadata: Optional[Dict[str, Any]] = None, is_image_generation: bool = False, orientation: Optional[str] = None):
264
383
  """
265
384
  Retries the prompt function if an error occurs.
266
385
  """
@@ -269,7 +388,7 @@ class MetaAI:
269
388
  f"Was unable to obtain a valid response from Meta AI. Retrying... Attempt {attempts + 1}/{MAX_RETRIES}."
270
389
  )
271
390
  time.sleep(3)
272
- return self.prompt(message, stream=stream, attempts=attempts + 1, media_ids=media_ids, attachment_metadata=attachment_metadata, is_image_generation=is_image_generation)
391
+ return self.prompt(message, stream=stream, attempts=attempts + 1, new_conversation=new_conversation, images=images, media_ids=media_ids, attachment_metadata=attachment_metadata, is_image_generation=is_image_generation, orientation=orientation)
273
392
  else:
274
393
  raise Exception(
275
394
  "Unable to obtain a valid response from Meta AI. Try again later."
@@ -587,6 +706,7 @@ class MetaAI:
587
706
  prompt: str,
588
707
  media_ids: Optional[list] = None,
589
708
  attachment_metadata: Optional[Dict[str, Any]] = None,
709
+ orientation: Optional[str] = None,
590
710
  wait_before_poll: int = 10,
591
711
  max_attempts: int = 30,
592
712
  wait_seconds: int = 5,
@@ -600,6 +720,7 @@ class MetaAI:
600
720
  prompt: Text prompt for video generation
601
721
  media_ids: Optional list of media IDs from uploaded images
602
722
  attachment_metadata: Optional dict with 'file_size' (int) and 'mime_type' (str)
723
+ orientation: Video orientation. Valid values: "LANDSCAPE", "VERTICAL", "SQUARE". Defaults to None.
603
724
  wait_before_poll: Seconds to wait before starting to poll (default: 10)
604
725
  max_attempts: Maximum polling attempts (default: 30)
605
726
  wait_seconds: Seconds between polling attempts (default: 5)
@@ -633,6 +754,7 @@ class MetaAI:
633
754
  prompt=prompt,
634
755
  media_ids=media_ids,
635
756
  attachment_metadata=attachment_metadata,
757
+ orientation=orientation,
636
758
  wait_before_poll=wait_before_poll,
637
759
  max_attempts=max_attempts,
638
760
  wait_seconds=wait_seconds,
@@ -92,6 +92,7 @@ class VideoGenerator:
92
92
  prompt: str,
93
93
  media_ids: Optional[List[str]] = None,
94
94
  attachment_metadata: Optional[Dict[str, Any]] = None,
95
+ orientation: Optional[str] = None,
95
96
  verbose: bool = True
96
97
  ) -> Dict:
97
98
  """
@@ -103,6 +104,7 @@ class VideoGenerator:
103
104
  prompt: Text prompt for video generation
104
105
  media_ids: Optional list of media IDs from uploaded images
105
106
  attachment_metadata: Optional dict with 'file_size' (int) and 'mime_type' (str)
107
+ orientation: Video orientation ("LANDSCAPE", "VERTICAL", "SQUARE"). Defaults to "VERTICAL".
106
108
  verbose: Whether to print status messages
107
109
 
108
110
  Returns:
@@ -113,11 +115,12 @@ class VideoGenerator:
113
115
  cookies_str="datr=...; abra_sess=...",
114
116
  prompt="Generate a video of a sunset",
115
117
  media_ids=["1234567890"],
116
- attachment_metadata={'file_size': 3310, 'mime_type': 'image/jpeg'}
118
+ attachment_metadata={'file_size': 3310, 'mime_type': 'image/jpeg'},
119
+ orientation="LANDSCAPE"
117
120
  )
118
121
  """
119
122
  generator = cls(cookies_str=cookies_str)
120
- return generator.generate_video(prompt=prompt, media_ids=media_ids, attachment_metadata=attachment_metadata, verbose=verbose)
123
+ return generator.generate_video(prompt=prompt, media_ids=media_ids, attachment_metadata=attachment_metadata, orientation=orientation, verbose=verbose)
121
124
 
122
125
  def build_headers(
123
126
  self,
@@ -169,6 +172,7 @@ class VideoGenerator:
169
172
  prompt_text: str,
170
173
  media_ids: Optional[List[str]] = None,
171
174
  attachment_metadata: Optional[Dict[str, Any]] = None,
175
+ orientation: Optional[str] = None,
172
176
  verbose: bool = True
173
177
  ) -> Optional[str]:
174
178
  """
@@ -178,6 +182,7 @@ class VideoGenerator:
178
182
  prompt_text: The text prompt for video generation
179
183
  media_ids: Optional list of media IDs from uploaded images
180
184
  attachment_metadata: Optional dict with 'file_size' (int) and 'mime_type' (str)
185
+ orientation: Video orientation ("LANDSCAPE", "VERTICAL", "SQUARE"). Defaults to "VERTICAL".
181
186
  verbose: Whether to print status messages
182
187
 
183
188
  Returns:
@@ -231,7 +236,7 @@ class VideoGenerator:
231
236
  "promptType": None,
232
237
  "artifactRewriteOptions": None,
233
238
  "imagineOperationRequest": None,
234
- "imagineClientOptions": {"orientation": "VERTICAL"},
239
+ "imagineClientOptions": {"orientation": orientation.upper() if orientation else "VERTICAL"},
235
240
  "spaceId": None,
236
241
  "sparkSnapshotId": None,
237
242
  "topicPageId": None,
@@ -306,7 +311,7 @@ class VideoGenerator:
306
311
  "__relay_internal__pv__KadabraSocialGraphrelayprovider": True
307
312
  }, separators=(',', ':'))
308
313
 
309
- print(variables)
314
+ # print(variables)
310
315
 
311
316
  # Build raw multipart body (exactly as in working example)
312
317
  spin_t = str(int(time.time()))
@@ -468,6 +473,9 @@ Content-Disposition: form-data; name="doc_id"\r
468
473
  Returns:
469
474
  List of video URLs
470
475
  """
476
+ import logging
477
+ logger = logging.getLogger(__name__)
478
+
471
479
  # Build headers with query-specific friendly name
472
480
  headers = self.build_headers(
473
481
  content_type='application/x-www-form-urlencoded',
@@ -566,6 +574,9 @@ Content-Disposition: form-data; name="doc_id"\r
566
574
 
567
575
  for attempt in range(1, max_attempts + 1):
568
576
  try:
577
+ if verbose and attempt % 5 == 1: # Log every 5th attempt
578
+ logger.info(f"[VIDEO POLLING] Attempt {attempt}/{max_attempts} for conversation {conversation_id}")
579
+
569
580
  response = requests.post(
570
581
  self.GRAPHQL_URL,
571
582
  cookies=self.cookies,
@@ -575,29 +586,44 @@ Content-Disposition: form-data; name="doc_id"\r
575
586
  )
576
587
 
577
588
  if response.status_code == 200:
578
- # Extract video URLs from response
579
- video_urls = self._extract_video_urls_from_response(response.text)
589
+ # Extract video URLs from response, passing attempt info to reduce noise
590
+ is_final_attempt = (attempt == max_attempts)
591
+ video_urls = self._extract_video_urls_from_response(
592
+ response.text,
593
+ is_final_attempt=is_final_attempt
594
+ )
580
595
 
581
596
  if video_urls:
597
+ if verbose:
598
+ logger.info(f"[VIDEO POLLING] ✓ Successfully extracted {len(video_urls)} video URL(s) on attempt {attempt}")
582
599
  return video_urls
583
600
  else:
601
+ if verbose and attempt % 5 == 0: # Log every 5th attempt
602
+ logger.debug(f"[VIDEO POLLING] No URLs yet on attempt {attempt}, continuing...")
584
603
  time.sleep(wait_seconds)
585
604
  else:
605
+ if verbose:
606
+ logger.warning(f"[VIDEO POLLING] HTTP {response.status_code} on attempt {attempt}")
586
607
  time.sleep(wait_seconds)
587
608
 
588
609
  except Exception as e:
610
+ if verbose:
611
+ logger.error(f"[VIDEO POLLING] Error on attempt {attempt}: {e}")
589
612
  time.sleep(wait_seconds)
590
613
 
614
+ if verbose:
615
+ logger.error(f"[VIDEO POLLING] ⚠️ Failed to extract video URLs after {max_attempts} attempts")
591
616
  return []
592
617
 
593
618
  @staticmethod
594
- def _extract_video_urls_from_response(response_text: str) -> List[str]:
619
+ def _extract_video_urls_from_response(response_text: str, is_final_attempt: bool = False) -> List[str]:
595
620
  """
596
621
  Extract video URLs from Meta AI GraphQL response.
597
622
  Uses the CORRECT structure from the original GitHub repo.
598
623
 
599
624
  Args:
600
625
  response_text: The response text to extract URLs from
626
+ is_final_attempt: Whether this is the final polling attempt (for logging)
601
627
 
602
628
  Returns:
603
629
  List of video URLs
@@ -605,15 +631,20 @@ Content-Disposition: form-data; name="doc_id"\r
605
631
  import logging
606
632
  logger = logging.getLogger(__name__)
607
633
 
608
- logger.info("[VIDEO URL EXTRACTION] Starting extraction with proper structure...")
609
- logger.debug(f"[VIDEO URL EXTRACTION] Response length: {len(response_text)} characters")
634
+ # Only log detailed extraction info on final attempt or when verbose debugging
635
+ log_details = is_final_attempt or logger.isEnabledFor(logging.DEBUG)
636
+
637
+ if log_details:
638
+ logger.debug("[VIDEO URL EXTRACTION] Starting extraction with proper structure...")
639
+ logger.debug(f"[VIDEO URL EXTRACTION] Response length: {len(response_text)} characters")
610
640
 
611
641
  urls: List[str] = []
612
642
 
613
643
  try:
614
644
  # Parse the response
615
645
  data = json.loads(response_text)
616
- logger.info("[VIDEO URL EXTRACTION] Successfully parsed response as JSON")
646
+ if log_details:
647
+ logger.debug("[VIDEO URL EXTRACTION] Successfully parsed response as JSON")
617
648
 
618
649
  # CORRECT STRUCTURE from original GitHub code:
619
650
  # data -> xfb_genai_fetch_post (or xab_abra__xfb_genai_fetch_post)
@@ -622,13 +653,14 @@ Content-Disposition: form-data; name="doc_id"\r
622
653
  data_obj = data.get("data", {})
623
654
  fetch_post = data_obj.get("xfb_genai_fetch_post") or data_obj.get("xab_abra__xfb_genai_fetch_post") or {}
624
655
 
625
- if not fetch_post:
626
- logger.warning("[VIDEO URL EXTRACTION] No xfb_genai_fetch_post or xab_abra__xfb_genai_fetch_post found in response")
627
- else:
628
- logger.info(f"[VIDEO URL EXTRACTION] Found fetch_post: {list(fetch_post.keys())}")
656
+ if not fetch_post and log_details:
657
+ logger.debug("[VIDEO URL EXTRACTION] No xfb_genai_fetch_post or xab_abra__xfb_genai_fetch_post found in response")
658
+ elif log_details:
659
+ logger.debug(f"[VIDEO URL EXTRACTION] Found fetch_post: {list(fetch_post.keys())}")
629
660
 
630
661
  messages = fetch_post.get("messages", {}).get("edges", [])
631
- logger.info(f"[VIDEO URL EXTRACTION] Found {len(messages)} message edges")
662
+ if log_details:
663
+ logger.debug(f"[VIDEO URL EXTRACTION] Found {len(messages)} message edges")
632
664
 
633
665
  for edge_idx, edge in enumerate(messages):
634
666
  node = edge.get("node", {})
@@ -636,62 +668,75 @@ Content-Disposition: form-data; name="doc_id"\r
636
668
  imagine_video = content.get("imagine_video") or {}
637
669
 
638
670
  if not imagine_video:
639
- logger.debug(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: No imagine_video found")
671
+ if log_details:
672
+ logger.debug(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: No imagine_video found")
640
673
  continue
641
674
 
642
- logger.info(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: Found imagine_video with keys: {list(imagine_video.keys())}")
675
+ if log_details:
676
+ logger.debug(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: Found imagine_video with keys: {list(imagine_video.keys())}")
643
677
 
644
678
  # Extract from videos.nodes[] array
645
679
  videos = imagine_video.get("videos", {}).get("nodes", [])
646
- logger.info(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: Found {len(videos)} video nodes")
680
+ if log_details:
681
+ logger.debug(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: Found {len(videos)} video nodes")
647
682
 
648
683
  for video_idx, video in enumerate(videos):
649
684
  # Try video_url or uri
650
685
  uri = video.get("video_url") or video.get("uri")
651
686
  if uri:
652
- logger.info(f"[VIDEO URL EXTRACTION] Found video_url/uri in videos.nodes[{video_idx}]: {uri[:100]}...")
687
+ if log_details:
688
+ logger.debug(f"[VIDEO URL EXTRACTION] Found video_url/uri in videos.nodes[{video_idx}]: {uri[:100]}...")
653
689
  urls.append(uri)
654
690
 
655
691
  # Try videoDeliveryResponseResult.progressive_urls[]
656
692
  delivery = video.get("videoDeliveryResponseResult") or {}
657
693
  prog = delivery.get("progressive_urls", [])
658
- logger.info(f"[VIDEO URL EXTRACTION] Found {len(prog)} progressive_urls in video {video_idx}")
694
+ if log_details and prog:
695
+ logger.debug(f"[VIDEO URL EXTRACTION] Found {len(prog)} progressive_urls in video {video_idx}")
659
696
 
660
697
  for prog_idx, p in enumerate(prog):
661
698
  pu = p.get("progressive_url")
662
699
  if pu:
663
- logger.info(f"[VIDEO URL EXTRACTION] Found progressive_url[{prog_idx}]: {pu[:100]}...")
700
+ if log_details:
701
+ logger.debug(f"[VIDEO URL EXTRACTION] Found progressive_url[{prog_idx}]: {pu[:100]}...")
664
702
  urls.append(pu)
665
703
 
666
704
  # Extract from single video object
667
705
  single_video = imagine_video.get("video") or {}
668
706
  if isinstance(single_video, dict) and single_video:
669
- logger.info(f"[VIDEO URL EXTRACTION] Found single video object with keys: {list(single_video.keys())}")
707
+ if log_details:
708
+ logger.debug(f"[VIDEO URL EXTRACTION] Found single video object with keys: {list(single_video.keys())}")
670
709
 
671
710
  uri = single_video.get("video_url") or single_video.get("uri")
672
711
  if uri:
673
- logger.info(f"[VIDEO URL EXTRACTION] Found video_url/uri in single video: {uri[:100]}...")
712
+ if log_details:
713
+ logger.debug(f"[VIDEO URL EXTRACTION] Found video_url/uri in single video: {uri[:100]}...")
674
714
  urls.append(uri)
675
715
 
676
716
  delivery = single_video.get("videoDeliveryResponseResult") or {}
677
717
  prog = delivery.get("progressive_urls", [])
678
- logger.info(f"[VIDEO URL EXTRACTION] Found {len(prog)} progressive_urls in single video")
718
+ if log_details and prog:
719
+ logger.debug(f"[VIDEO URL EXTRACTION] Found {len(prog)} progressive_urls in single video")
679
720
 
680
721
  for prog_idx, p in enumerate(prog):
681
722
  pu = p.get("progressive_url")
682
723
  if pu:
683
- logger.info(f"[VIDEO URL EXTRACTION] Found progressive_url[{prog_idx}]: {pu[:100]}...")
724
+ if log_details:
725
+ logger.debug(f"[VIDEO URL EXTRACTION] Found progressive_url[{prog_idx}]: {pu[:100]}...")
684
726
  urls.append(pu)
685
727
 
686
728
  except json.JSONDecodeError as e:
687
- logger.error(f"[VIDEO URL EXTRACTION] JSON decode failed: {e}")
688
- logger.debug(f"[VIDEO URL EXTRACTION] Response preview: {response_text[:500]}")
729
+ if is_final_attempt:
730
+ logger.error(f"[VIDEO URL EXTRACTION] JSON decode failed: {e}")
731
+ logger.debug(f"[VIDEO URL EXTRACTION] Response preview: {response_text[:500]}")
689
732
 
690
733
  # Fallback to regex
691
- logger.info("[VIDEO URL EXTRACTION] Falling back to regex extraction...")
734
+ if log_details:
735
+ logger.debug("[VIDEO URL EXTRACTION] Falling back to regex extraction...")
692
736
  import re
693
737
  urls = re.findall(r'https?://[^\s"\'<>]+fbcdn[^\s"\'<>]+\.mp4[^\s"\'<>]*', response_text)
694
- logger.info(f"[VIDEO URL EXTRACTION] Regex found {len(urls)} .mp4 URLs")
738
+ if log_details:
739
+ logger.debug(f"[VIDEO URL EXTRACTION] Regex found {len(urls)} .mp4 URLs")
695
740
 
696
741
  # Deduplicate while preserving order
697
742
  seen = set()
@@ -701,11 +746,13 @@ Content-Disposition: form-data; name="doc_id"\r
701
746
  seen.add(u)
702
747
  unique_urls.append(u)
703
748
 
704
- logger.info(f"[VIDEO URL EXTRACTION] Final result: {len(unique_urls)} unique video URLs")
749
+ # Only log final result if we have URLs or if this is the final attempt
705
750
  if unique_urls:
706
- for idx, url in enumerate(unique_urls, 1):
707
- logger.info(f"[VIDEO URL EXTRACTION] Final URL {idx}: {url[:150]}...")
708
- else:
751
+ if log_details:
752
+ logger.debug(f"[VIDEO URL EXTRACTION] Final result: {len(unique_urls)} unique video URLs")
753
+ for idx, url in enumerate(unique_urls, 1):
754
+ logger.debug(f"[VIDEO URL EXTRACTION] Final URL {idx}: {url[:150]}...")
755
+ elif is_final_attempt:
709
756
  logger.warning("[VIDEO URL EXTRACTION] ⚠️ NO VIDEO URLs FOUND!")
710
757
  logger.debug(f"[VIDEO URL EXTRACTION] Response preview (first 1000 chars): {response_text[:1000]}")
711
758
 
@@ -716,6 +763,7 @@ Content-Disposition: form-data; name="doc_id"\r
716
763
  prompt: str,
717
764
  media_ids: Optional[List[str]] = None,
718
765
  attachment_metadata: Optional[Dict[str, Any]] = None,
766
+ orientation: Optional[str] = None,
719
767
  wait_before_poll: int = 10,
720
768
  max_attempts: int = 30,
721
769
  wait_seconds: int = 5,
@@ -728,6 +776,7 @@ Content-Disposition: form-data; name="doc_id"\r
728
776
  prompt: Text prompt for video generation
729
777
  media_ids: Optional list of media IDs from uploaded images
730
778
  attachment_metadata: Optional dict with 'file_size' (int) and 'mime_type' (str)
779
+ orientation: Video orientation ("LANDSCAPE", "VERTICAL", "SQUARE"). Defaults to "VERTICAL".
731
780
  wait_before_poll: Seconds to wait before starting to poll
732
781
  max_attempts: Maximum polling attempts
733
782
  wait_seconds: Seconds between polling attempts
@@ -741,6 +790,7 @@ Content-Disposition: form-data; name="doc_id"\r
741
790
  prompt_text=prompt,
742
791
  media_ids=media_ids,
743
792
  attachment_metadata=attachment_metadata,
793
+ orientation=orientation,
744
794
  verbose=verbose
745
795
  )
746
796
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metaai-sdk
3
- Version: 2.3.4
3
+ Version: 2.3.5
4
4
  Summary: Feature-rich Python SDK for Meta AI - Chat, Image & Video Generation powered by Llama 3
5
5
  Author-email: Ashiq Hussain Mir <imseldrith@gmail.com>
6
6
  License-Expression: MIT
@@ -27,8 +27,8 @@ Requires-Dist: lxml-html-clean>=0.1.1
27
27
  Requires-Dist: beautifulsoup4>=4.9.0
28
28
  Requires-Dist: python-multipart>=0.0.21
29
29
  Provides-Extra: api
30
- Requires-Dist: fastapi<0.96.0,>=0.95.2; extra == "api"
31
- Requires-Dist: uvicorn[standard]<0.24.0,>=0.22.0; extra == "api"
30
+ Requires-Dist: fastapi>=0.95.2; extra == "api"
31
+ Requires-Dist: uvicorn[standard]>=0.22.0; extra == "api"
32
32
  Requires-Dist: python-multipart>=0.0.6; extra == "api"
33
33
  Requires-Dist: python-dotenv>=1.0.0; extra == "api"
34
34
  Provides-Extra: dev
@@ -487,22 +487,29 @@ print(f"\n🎉 Generated {len(videos)} videos successfully!")
487
487
  🎉 Generated 3 videos successfully!
488
488
  ```
489
489
 
490
- ### Example 3: Advanced Video Generation
490
+ ### Example 3: Advanced Video Generation with Orientation
491
491
 
492
492
  ```python
493
493
  from metaai_api import MetaAI
494
494
 
495
495
  ai = MetaAI(cookies=cookies)
496
496
 
497
- # Fine-tune generation parameters
497
+ # Generate video with specific orientation (default is VERTICAL)
498
498
  result = ai.generate_video(
499
499
  prompt="A time-lapse of a flower blooming",
500
+ orientation="VERTICAL", # Options: "LANDSCAPE", "VERTICAL", "SQUARE"
500
501
  wait_before_poll=15, # Wait 15 seconds before checking
501
502
  max_attempts=50, # Try up to 50 times
502
503
  wait_seconds=3, # Wait 3 seconds between attempts
503
504
  verbose=True # Show detailed progress
504
505
  )
505
506
 
507
+ # Generate landscape video for widescreen
508
+ result_landscape = ai.generate_video(
509
+ prompt="Panoramic view of sunset over mountains",
510
+ orientation="LANDSCAPE" # Wide format (16:9)
511
+ )
512
+
506
513
  if result["success"]:
507
514
  print(f"\n🎬 Your videos are ready!")
508
515
  print(f"🔗 Generated {len(result['video_urls'])} videos:")
@@ -511,6 +518,14 @@ if result["success"]:
511
518
  print(f"⏱️ Generated at: {result['timestamp']}")
512
519
  ```
513
520
 
521
+ **Supported Video Orientations:**
522
+
523
+ - `"LANDSCAPE"` - Wide/horizontal (16:9) - ideal for widescreen, cinematic content
524
+ - `"VERTICAL"` - Tall/vertical (9:16) - ideal for mobile, stories, reels (default)
525
+ - `"SQUARE"` - Equal dimensions (1:1) - ideal for social posts
526
+
527
+ ````
528
+
514
529
  📖 **Full Video Guide:** See [VIDEO_GENERATION_README.md](https://github.com/mir-ashiq/metaai-api/blob/main/VIDEO_GENERATION_README.md) for complete documentation!
515
530
 
516
531
  ---
@@ -565,7 +580,7 @@ if result["success"]:
565
580
  )
566
581
  if video["success"]:
567
582
  print(f"🎬 Video: {video['video_urls'][0]}")
568
- ```
583
+ ````
569
584
 
570
585
  **Output:**
571
586
 
@@ -583,7 +598,7 @@ if result["success"]:
583
598
 
584
599
  ## 🎨 Image Generation
585
600
 
586
- Generate AI-powered images (requires Facebook authentication):
601
+ Generate AI-powered images with customizable orientations (requires Facebook authentication):
587
602
 
588
603
  ```python
589
604
  from metaai_api import MetaAI
@@ -591,9 +606,25 @@ from metaai_api import MetaAI
591
606
  # Initialize with Facebook credentials
592
607
  ai = MetaAI(fb_email="your_email@example.com", fb_password="your_password")
593
608
 
594
- # Generate images
609
+ # Generate images with default orientation (VERTICAL)
595
610
  response = ai.prompt("Generate an image of a cyberpunk cityscape at night with neon lights")
596
611
 
612
+ # Or specify orientation explicitly
613
+ response_landscape = ai.prompt(
614
+ "Generate an image of a panoramic mountain landscape",
615
+ orientation="LANDSCAPE" # Options: "LANDSCAPE", "VERTICAL", "SQUARE"
616
+ )
617
+
618
+ response_vertical = ai.prompt(
619
+ "Generate an image of a tall waterfall",
620
+ orientation="VERTICAL" # Tall/portrait format (default)
621
+ )
622
+
623
+ response_square = ai.prompt(
624
+ "Generate an image of a centered mandala pattern",
625
+ orientation="SQUARE" # Square format (1:1)
626
+ )
627
+
597
628
  # Display results (Meta AI generates 4 images by default)
598
629
  print(f"🎨 Generated {len(response['media'])} images:")
599
630
  for i, image in enumerate(response['media'], 1):
@@ -601,6 +632,12 @@ for i, image in enumerate(response['media'], 1):
601
632
  print(f" Prompt: {image['prompt']}")
602
633
  ```
603
634
 
635
+ **Supported Orientations:**
636
+
637
+ - `"LANDSCAPE"` - Wide/horizontal format (16:9) - ideal for panoramas, landscapes
638
+ - `"VERTICAL"` - Tall/vertical format (9:16) - ideal for portraits, mobile content (default)
639
+ - `"SQUARE"` - Equal dimensions (1:1) - ideal for social media, profile images
640
+
604
641
  **Output:**
605
642
 
606
643
  ```
@@ -680,7 +717,6 @@ class MetaAI:
680
717
  **Methods:**
681
718
 
682
719
  - **`prompt(message, stream=False, new_conversation=False)`**
683
-
684
720
  - Send a chat message
685
721
  - Returns: `dict` with `message`, `sources`, and `media`
686
722
 
@@ -3,11 +3,11 @@ metaai_api/api_server.py,sha256=hb1C3rEarPOHF6W-qqENaxrZWwn8A0qUrjSVlRtNV2s,1324
3
3
  metaai_api/client.py,sha256=Th46qW1l8OS8Hu5pj0aGFn4iQNz62A3sbXko-LP-SAU,5263
4
4
  metaai_api/exceptions.py,sha256=MRRAppZa0OFA0QLSvC0nABgZN_Ll1dUq9JfhECTqV-Q,114
5
5
  metaai_api/image_upload.py,sha256=DQ2xqKdM1I_pF1rZBsB7-QTvXLzke2_0XiIOxFhpc70,6563
6
- metaai_api/main.py,sha256=3kWYikKjq7pk2l8x6h12OkHe2pwxcs_UxzLj2qSy9Qs,28384
6
+ metaai_api/main.py,sha256=bZrao6uZI-CdtHvdu64jUH72h0R7atN2PNrP5TaHpMM,36738
7
7
  metaai_api/utils.py,sha256=qzfIO3WkRH-gSV99b8RiECnMOku8lZEY3Jka9lTLExA,11979
8
- metaai_api/video_generation.py,sha256=NlNLvzzZqHw-nvbY9ATvn4BDusHs2luOYDrOSvhMWnk,34537
9
- metaai_sdk-2.3.4.dist-info/licenses/LICENSE,sha256=hRLLSBixyX0tRh2k0iOGoF7nx-l-vBChNffFfVOIEtc,1290
10
- metaai_sdk-2.3.4.dist-info/METADATA,sha256=yR6jKg1VXjGZSzPYydKF4ucOMu0Nd79Xr9XkhzrOioc,27602
11
- metaai_sdk-2.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- metaai_sdk-2.3.4.dist-info/top_level.txt,sha256=R6YCiIQLYFKKaqhNZXDwXbpj1u01P_YhcMCVbJiDUJs,11
13
- metaai_sdk-2.3.4.dist-info/RECORD,,
8
+ metaai_api/video_generation.py,sha256=Kw0Hwqeai6EexPhJsN-0eLSmIL0zDp95EwAPk6UQ9Rs,37480
9
+ metaai_sdk-2.3.5.dist-info/licenses/LICENSE,sha256=hRLLSBixyX0tRh2k0iOGoF7nx-l-vBChNffFfVOIEtc,1290
10
+ metaai_sdk-2.3.5.dist-info/METADATA,sha256=adQytqJvLOkHunqgkZet0ImIU74pTSbZK2udaC0utN4,28998
11
+ metaai_sdk-2.3.5.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
12
+ metaai_sdk-2.3.5.dist-info/top_level.txt,sha256=R6YCiIQLYFKKaqhNZXDwXbpj1u01P_YhcMCVbJiDUJs,11
13
+ metaai_sdk-2.3.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5