metaai-sdk 2.3.3__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,
@@ -0,0 +1,819 @@
1
+ """
2
+ Meta AI Video Generation Module
3
+ Advanced video generation and retrieval using Meta AI's GraphQL API
4
+ """
5
+
6
+ import requests
7
+ import json
8
+ import time
9
+ import uuid
10
+ from typing import Dict, List, Optional, Any
11
+ from requests_html import HTMLSession
12
+ from metaai_api.utils import extract_value
13
+
14
+
15
+ class VideoGenerator:
16
+ """
17
+ A class to handle video generation requests to Meta AI's GraphQL API.
18
+ Supports creating videos from text prompts and retrieving video URLs.
19
+ """
20
+
21
+ GRAPHQL_URL = 'https://www.meta.ai/api/graphql/'
22
+
23
+ def __init__(
24
+ self,
25
+ cookies_str: Optional[str] = None,
26
+ cookies_dict: Optional[Dict[str, str]] = None
27
+ ):
28
+ """
29
+ Initialize the VideoGenerator.
30
+ Automatically fetches lsd and fb_dtsg tokens from Meta AI.
31
+
32
+ Args:
33
+ cookies_str: Cookie string in format "key=value; key=value"
34
+ cookies_dict: Pre-parsed cookies dictionary
35
+ """
36
+ if cookies_dict:
37
+ self.cookies = cookies_dict
38
+ self.cookies_str = "; ".join([f"{k}={v}" for k, v in cookies_dict.items()])
39
+ elif cookies_str:
40
+ self.cookies = self._parse_cookies(cookies_str)
41
+ self.cookies_str = cookies_str
42
+ else:
43
+ raise ValueError("Either cookies_str or cookies_dict must be provided")
44
+
45
+ # Always auto-fetch tokens
46
+ try:
47
+ tokens = self.get_lsd_and_dtsg(self.cookies_str)
48
+ self.lsd = tokens['lsd']
49
+ self.fb_dtsg = tokens['fb_dtsg']
50
+ except Exception as e:
51
+ raise ValueError(f"Failed to auto-fetch tokens: {e}")
52
+
53
+ @staticmethod
54
+ def _parse_cookies(cookie_str: str) -> Dict[str, str]:
55
+ """Parse cookie string into dictionary"""
56
+ cookies = {}
57
+ for item in cookie_str.split('; '):
58
+ if '=' in item:
59
+ key, value = item.split('=', 1)
60
+ cookies[key] = value
61
+ return cookies
62
+
63
+ @staticmethod
64
+ def get_lsd_and_dtsg(cookies_str: str) -> Dict[str, str]:
65
+ """
66
+ Extract lsd and fb_dtsg from Meta AI page using provided cookies.
67
+
68
+ Args:
69
+ cookies_str: Cookie string in format "key1=value1; key2=value2; ..."
70
+
71
+ Returns:
72
+ Dictionary with 'lsd' and 'fb_dtsg' keys
73
+ """
74
+ # Fetch Meta AI page with cookies
75
+ session = HTMLSession()
76
+ headers = {"cookie": cookies_str}
77
+ response = session.get("https://www.meta.ai/", headers=headers)
78
+
79
+ # Extract lsd and fb_dtsg
80
+ lsd = extract_value(response.text, start_str='"LSD",[],{"token":"', end_str='"')
81
+ fb_dtsg = extract_value(response.text, start_str='DTSGInitData",[],{"token":"', end_str='"')
82
+
83
+ return {
84
+ "lsd": lsd,
85
+ "fb_dtsg": fb_dtsg
86
+ }
87
+
88
+ @classmethod
89
+ def quick_generate(
90
+ cls,
91
+ cookies_str: str,
92
+ prompt: str,
93
+ media_ids: Optional[List[str]] = None,
94
+ attachment_metadata: Optional[Dict[str, Any]] = None,
95
+ orientation: Optional[str] = None,
96
+ verbose: bool = True
97
+ ) -> Dict:
98
+ """
99
+ Convenience method to generate a video with minimal setup.
100
+ Automatically fetches tokens and generates video in one call.
101
+
102
+ Args:
103
+ cookies_str: Cookie string in format "key1=value1; key2=value2; ..."
104
+ prompt: Text prompt for video generation
105
+ media_ids: Optional list of media IDs from uploaded images
106
+ attachment_metadata: Optional dict with 'file_size' (int) and 'mime_type' (str)
107
+ orientation: Video orientation ("LANDSCAPE", "VERTICAL", "SQUARE"). Defaults to "VERTICAL".
108
+ verbose: Whether to print status messages
109
+
110
+ Returns:
111
+ Dictionary with success status, conversation_id, prompt, video_urls, and timestamp
112
+
113
+ Example:
114
+ result = VideoGenerator.quick_generate(
115
+ cookies_str="datr=...; abra_sess=...",
116
+ prompt="Generate a video of a sunset",
117
+ media_ids=["1234567890"],
118
+ attachment_metadata={'file_size': 3310, 'mime_type': 'image/jpeg'},
119
+ orientation="LANDSCAPE"
120
+ )
121
+ """
122
+ generator = cls(cookies_str=cookies_str)
123
+ return generator.generate_video(prompt=prompt, media_ids=media_ids, attachment_metadata=attachment_metadata, orientation=orientation, verbose=verbose)
124
+
125
+ def build_headers(
126
+ self,
127
+ content_type: str = 'application/x-www-form-urlencoded',
128
+ friendly_name: Optional[str] = None,
129
+ additional_headers: Optional[Dict[str, str]] = None
130
+ ) -> Dict[str, str]:
131
+ """
132
+ Build dynamic headers for Meta AI API requests.
133
+
134
+ Args:
135
+ content_type: Content-Type header value
136
+ friendly_name: Optional X-FB-Friendly-Name for the request
137
+ additional_headers: Optional dict of additional headers to merge
138
+
139
+ Returns:
140
+ Complete headers dictionary
141
+ """
142
+ # Base headers common to all requests
143
+ headers = {
144
+ 'accept': '*/*',
145
+ 'accept-language': 'en-US,en;q=0.5',
146
+ 'content-type': content_type,
147
+ 'origin': 'https://www.meta.ai',
148
+ 'referer': 'https://www.meta.ai/',
149
+ 'sec-ch-ua': '"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
150
+ 'sec-ch-ua-mobile': '?0',
151
+ 'sec-ch-ua-platform': '"Windows"',
152
+ 'sec-fetch-dest': 'empty',
153
+ 'sec-fetch-mode': 'cors',
154
+ 'sec-fetch-site': 'same-origin',
155
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
156
+ 'x-asbd-id': '359341',
157
+ 'x-fb-lsd': self.lsd,
158
+ }
159
+
160
+ # Add optional friendly name
161
+ if friendly_name:
162
+ headers['x-fb-friendly-name'] = friendly_name
163
+
164
+ # Add additional headers specific to request type
165
+ if additional_headers:
166
+ headers.update(additional_headers)
167
+
168
+ return headers
169
+
170
+ def create_video_generation_request(
171
+ self,
172
+ prompt_text: str,
173
+ media_ids: Optional[List[str]] = None,
174
+ attachment_metadata: Optional[Dict[str, Any]] = None,
175
+ orientation: Optional[str] = None,
176
+ verbose: bool = True
177
+ ) -> Optional[str]:
178
+ """
179
+ Send video generation request to Meta AI using raw multipart body.
180
+
181
+ Args:
182
+ prompt_text: The text prompt for video generation
183
+ media_ids: Optional list of media IDs from uploaded images
184
+ attachment_metadata: Optional dict with 'file_size' (int) and 'mime_type' (str)
185
+ orientation: Video orientation ("LANDSCAPE", "VERTICAL", "SQUARE"). Defaults to "VERTICAL".
186
+ verbose: Whether to print status messages
187
+
188
+ Returns:
189
+ external_conversation_id if successful, None otherwise
190
+ """
191
+ # Generate unique IDs
192
+ external_conversation_id = str(uuid.uuid4())
193
+ offline_threading_id = str(int(time.time() * 1000000000))[:19]
194
+ thread_session_id = str(uuid.uuid4())
195
+ bot_offline_threading_id = str(int(time.time() * 1000000000) + 1)[:19]
196
+ qpl_join_id = str(uuid.uuid4()).replace('-', '')
197
+
198
+ # Build headers with multipart-specific additions
199
+ multipart_headers = {
200
+ 'priority': 'u=1, i',
201
+ 'sec-ch-ua-full-version-list': '"Brave";v="141.0.0.0", "Not?A_Brand";v="8.0.0.0", "Chromium";v="141.0.0.0"',
202
+ 'sec-ch-ua-model': '""',
203
+ 'sec-ch-ua-platform-version': '"19.0.0"',
204
+ 'sec-gpc': '1',
205
+ }
206
+
207
+ headers = self.build_headers(
208
+ content_type='multipart/form-data; boundary=----WebKitFormBoundaryu59CeaZS4ag939lz',
209
+ additional_headers=multipart_headers
210
+ )
211
+
212
+ # Build variables JSON (compact, no extra spaces)
213
+ variables = json.dumps({
214
+ "message": {"sensitive_string_value": prompt_text},
215
+ "externalConversationId": external_conversation_id,
216
+ "offlineThreadingId": offline_threading_id,
217
+ "threadSessionId": thread_session_id,
218
+ "isNewConversation": True,
219
+ "suggestedPromptIndex": None,
220
+ "promptPrefix": None,
221
+ "entrypoint": "KADABRA__CHAT__UNIFIED_INPUT_BAR",
222
+ "attachments": [],
223
+ "attachmentsV2": media_ids if media_ids else [],
224
+ "activeMediaSets": [],
225
+ "activeCardVersions": [],
226
+ "activeArtifactVersion": None,
227
+ "userUploadEditModeInput": None,
228
+ "reelComposeInput": None,
229
+ "qplJoinId": qpl_join_id,
230
+ "sourceRemixPostId": None,
231
+ "gkPlannerOrReasoningEnabled": True,
232
+ "selectedModel": "BASIC_OPTION",
233
+ "conversationMode": None,
234
+ "selectedAgentType": "PLANNER",
235
+ "conversationStarterId": None,
236
+ "promptType": None,
237
+ "artifactRewriteOptions": None,
238
+ "imagineOperationRequest": None,
239
+ "imagineClientOptions": {"orientation": orientation.upper() if orientation else "VERTICAL"},
240
+ "spaceId": None,
241
+ "sparkSnapshotId": None,
242
+ "topicPageId": None,
243
+ "includeSpace": False,
244
+ "storybookId": None,
245
+ "messagePersistentInput": {
246
+ "attachment_size": attachment_metadata.get('file_size') if attachment_metadata else None,
247
+ "attachment_type": attachment_metadata.get('mime_type') if attachment_metadata else None,
248
+ "bot_message_offline_threading_id": bot_offline_threading_id,
249
+ "conversation_mode": None,
250
+ "external_conversation_id": external_conversation_id,
251
+ "is_new_conversation": True,
252
+ "meta_ai_entry_point": "KADABRA__CHAT__UNIFIED_INPUT_BAR",
253
+ "offline_threading_id": offline_threading_id,
254
+ "prompt_id": None,
255
+ "prompt_session_id": thread_session_id
256
+ },
257
+ "alakazam_enabled": True,
258
+ "skipInFlightMessageWithParams": None,
259
+ "__relay_internal__pv__KadabraSocialSearchEnabledrelayprovider": False,
260
+ "__relay_internal__pv__KadabraZeitgeistEnabledrelayprovider": False,
261
+ "__relay_internal__pv__alakazam_enabledrelayprovider": True,
262
+ "__relay_internal__pv__sp_kadabra_survey_invitationrelayprovider": True,
263
+ "__relay_internal__pv__KadabraAINativeUXrelayprovider": False,
264
+ "__relay_internal__pv__enable_kadabra_partial_resultsrelayprovider": False,
265
+ "__relay_internal__pv__AbraArtifactsEnabledrelayprovider": True,
266
+ "__relay_internal__pv__KadabraMemoryEnabledrelayprovider": False,
267
+ "__relay_internal__pv__AbraPlannerEnabledrelayprovider": True,
268
+ "__relay_internal__pv__AbraWidgetsEnabledrelayprovider": False,
269
+ "__relay_internal__pv__KadabraDeepResearchEnabledrelayprovider": False,
270
+ "__relay_internal__pv__KadabraThinkHarderEnabledrelayprovider": False,
271
+ "__relay_internal__pv__KadabraVergeEnabledrelayprovider": False,
272
+ "__relay_internal__pv__KadabraSpacesEnabledrelayprovider": False,
273
+ "__relay_internal__pv__KadabraProductSearchEnabledrelayprovider": False,
274
+ "__relay_internal__pv__KadabraAreServiceEnabledrelayprovider": False,
275
+ "__relay_internal__pv__kadabra_render_reasoning_response_statesrelayprovider": True,
276
+ "__relay_internal__pv__kadabra_reasoning_cotrelayprovider": False,
277
+ "__relay_internal__pv__AbraSearchInlineReferencesEnabledrelayprovider": True,
278
+ "__relay_internal__pv__AbraComposedTextWidgetsrelayprovider": True,
279
+ "__relay_internal__pv__KadabraNewCitationsEnabledrelayprovider": True,
280
+ "__relay_internal__pv__WebPixelRatiorelayprovider": 1,
281
+ "__relay_internal__pv__KadabraVideoDeliveryRequestrelayprovider": {
282
+ "dash_manifest_requests": [{}],
283
+ "progressive_url_requests": [{"quality": "HD"}, {"quality": "SD"}]
284
+ },
285
+ "__relay_internal__pv__KadabraWidgetsRedesignEnabledrelayprovider": False,
286
+ "__relay_internal__pv__kadabra_enable_send_message_retryrelayprovider": True,
287
+ "__relay_internal__pv__KadabraEmailCalendarIntegrationrelayprovider": False,
288
+ "__relay_internal__pv__kadabra_reels_connect_featuresrelayprovider": False,
289
+ "__relay_internal__pv__AbraBugNubrelayprovider": False,
290
+ "__relay_internal__pv__AbraRedteamingrelayprovider": False,
291
+ "__relay_internal__pv__AbraDebugDevOnlyrelayprovider": False,
292
+ "__relay_internal__pv__kadabra_enable_open_in_editor_message_actionrelayprovider": True,
293
+ "__relay_internal__pv__AbraThreadsEnabledrelayprovider": False,
294
+ "__relay_internal__pv__kadabra_story_builder_enabledrelayprovider": False,
295
+ "__relay_internal__pv__kadabra_imagine_canvas_enable_dev_settingsrelayprovider": False,
296
+ "__relay_internal__pv__kadabra_create_media_deletionrelayprovider": False,
297
+ "__relay_internal__pv__kadabra_moodboardrelayprovider": False,
298
+ "__relay_internal__pv__AbraArtifactDragImagineFromConversationrelayprovider": True,
299
+ "__relay_internal__pv__kadabra_media_item_renderer_heightrelayprovider": 545,
300
+ "__relay_internal__pv__kadabra_media_item_renderer_widthrelayprovider": 620,
301
+ "__relay_internal__pv__AbraQPDocUploadNuxTriggerNamerelayprovider": "meta_dot_ai_abra_web_doc_upload_nux_tour",
302
+ "__relay_internal__pv__AbraSurfaceNuxIDrelayprovider": "12177",
303
+ "__relay_internal__pv__KadabraConversationRenamingrelayprovider": True,
304
+ "__relay_internal__pv__AbraIsLoggedOutrelayprovider": False,
305
+ "__relay_internal__pv__KadabraCanvasDisplayHeaderV2relayprovider": False,
306
+ "__relay_internal__pv__AbraArtifactEditorDebugModerelayprovider": False,
307
+ "__relay_internal__pv__AbraArtifactEditorDownloadHTMLEnabledrelayprovider": False,
308
+ "__relay_internal__pv__kadabra_create_row_hover_optionsrelayprovider": False,
309
+ "__relay_internal__pv__kadabra_media_info_pillsrelayprovider": True,
310
+ "__relay_internal__pv__KadabraConcordInternalProfileBadgeEnabledrelayprovider": False,
311
+ "__relay_internal__pv__KadabraSocialGraphrelayprovider": True
312
+ }, separators=(',', ':'))
313
+
314
+ # print(variables)
315
+
316
+ # Build raw multipart body (exactly as in working example)
317
+ spin_t = str(int(time.time()))
318
+ body = f"""------WebKitFormBoundaryu59CeaZS4ag939lz\r
319
+ Content-Disposition: form-data; name="av"\r
320
+ \r
321
+ 813590375178585\r
322
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
323
+ Content-Disposition: form-data; name="__user"\r
324
+ \r
325
+ 0\r
326
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
327
+ Content-Disposition: form-data; name="__a"\r
328
+ \r
329
+ 1\r
330
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
331
+ Content-Disposition: form-data; name="__req"\r
332
+ \r
333
+ q\r
334
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
335
+ Content-Disposition: form-data; name="__hs"\r
336
+ \r
337
+ 20413.HYP:kadabra_pkg.2.1...0\r
338
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
339
+ Content-Disposition: form-data; name="dpr"\r
340
+ \r
341
+ 1\r
342
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
343
+ Content-Disposition: form-data; name="__ccg"\r
344
+ \r
345
+ GOOD\r
346
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
347
+ Content-Disposition: form-data; name="__rev"\r
348
+ \r
349
+ 1030219547\r
350
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
351
+ Content-Disposition: form-data; name="__s"\r
352
+ \r
353
+ q59jx4:9bnqdw:3ats33\r
354
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
355
+ Content-Disposition: form-data; name="__hsi"\r
356
+ \r
357
+ 7575127759957881428\r
358
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
359
+ Content-Disposition: form-data; name="__dyn"\r
360
+ \r
361
+ 7xeUjG1mxu1syUqxemh0no6u5U4e2C1vzEdE98K360CEbo1nEhw2nVEtwMw6ywaq221FwpUO0n24oaEnxO0Bo7O2l0Fwqo31w9O1lwlE-U2zxe2GewbS361qw82dUlwhE-15wmo423-0j52oS0Io5d0bS1LBwNwKG0WE8oC1IwGw-wlUcE2-G2O7E5y1rwa211wo84y1iwfe1aw\r
362
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
363
+ Content-Disposition: form-data; name="__csr"\r
364
+ \r
365
+ gaJNBjWsAJvliQPqlWFFknAiUB2bBjWLmhyblepaGyVFGy8y2i5pEW68mwwwPwxgtNgv2AMEu6PAgrCwc7F212xxe5YyVC1pAg01sq99uQ1zK0dp75gKzAy8y0EjcgQ8Ek0yMJC6G1og5KrXD4GexS8wdasU8U1e4075UeEuwfCA8K0hWiU2tAyE5m0gm0Jo0xUGxh1veU0gGyWfe0iK1xo32yXhoKkw56pwMw1e25onU4i0TA0xaxu00B1Q2ha2K3V0eqCmawnEgg2Gw\r
366
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
367
+ Content-Disposition: form-data; name="__hsdp"\r
368
+ \r
369
+ gdDdNhMlJ8bNG7i42aHgWzckH57ylAt8NkkOGCVQ8Ay8myETxW1vh48gHx-UC9Bgpy87G0BUfU7i0JFUeo7Cm12wlo5OawRwDwzxW1zg33wgodU\r
370
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
371
+ Content-Disposition: form-data; name="__hblp"\r
372
+ \r
373
+ 08W5EWt0BzUWp5Q4vz4HOk5kVogDGqmHgyi8xq9gNrxG1vh8B2K6pry4mVk8x28wuE5a1DxO1Qwr84Cu3C1VBwCxK2W2qi2y1LwDwzyK445Gwi63-0wUkxa9AyEjgogy3-\r
374
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
375
+ Content-Disposition: form-data; name="__sjsp"\r
376
+ \r
377
+ gdDtsAFMlJ8bNG7i47AG5lxmUmDiFQca9U\r
378
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
379
+ Content-Disposition: form-data; name="__comet_req"\r
380
+ \r
381
+ 72\r
382
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
383
+ Content-Disposition: form-data; name="fb_dtsg"\r
384
+ \r
385
+ {self.fb_dtsg}\r
386
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
387
+ Content-Disposition: form-data; name="jazoest"\r
388
+ \r
389
+ 25499\r
390
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
391
+ Content-Disposition: form-data; name="lsd"\r
392
+ \r
393
+ {self.lsd}\r
394
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
395
+ Content-Disposition: form-data; name="__spin_r"\r
396
+ \r
397
+ 1030219547\r
398
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
399
+ Content-Disposition: form-data; name="__spin_b"\r
400
+ \r
401
+ trunk\r
402
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
403
+ Content-Disposition: form-data; name="__spin_t"\r
404
+ \r
405
+ {spin_t}\r
406
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
407
+ Content-Disposition: form-data; name="__jssesw"\r
408
+ \r
409
+ 1\r
410
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
411
+ Content-Disposition: form-data; name="__crn"\r
412
+ \r
413
+ comet.kadabra.KadabraAssistantRoute\r
414
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
415
+ Content-Disposition: form-data; name="fb_api_caller_class"\r
416
+ \r
417
+ RelayModern\r
418
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
419
+ Content-Disposition: form-data; name="fb_api_req_friendly_name"\r
420
+ \r
421
+ useKadabraSendMessageMutation\r
422
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
423
+ Content-Disposition: form-data; name="server_timestamps"\r
424
+ \r
425
+ true\r
426
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
427
+ Content-Disposition: form-data; name="variables"\r
428
+ \r
429
+ {variables}\r
430
+ ------WebKitFormBoundaryu59CeaZS4ag939lz\r
431
+ Content-Disposition: form-data; name="doc_id"\r
432
+ \r
433
+ 25290947477183545\r
434
+ ------WebKitFormBoundaryu59CeaZS4ag939lz--\r
435
+ """
436
+
437
+ # URL with query parameters
438
+ url = f"{self.GRAPHQL_URL}?fb_dtsg={self.fb_dtsg}&jazoest=25499&lsd={self.lsd}"
439
+
440
+ try:
441
+ response = requests.post(
442
+ url,
443
+ cookies=self.cookies,
444
+ headers=headers,
445
+ data=body.encode('utf-8'),
446
+ timeout=30
447
+ )
448
+
449
+ if response.status_code == 200:
450
+ return external_conversation_id
451
+ else:
452
+ return None
453
+
454
+ except Exception as e:
455
+ return None
456
+
457
+ def fetch_video_urls(
458
+ self,
459
+ conversation_id: str,
460
+ max_attempts: int = 30,
461
+ wait_seconds: int = 5,
462
+ verbose: bool = True
463
+ ) -> List[str]:
464
+ """
465
+ Poll for video URLs from a conversation.
466
+
467
+ Args:
468
+ conversation_id: The conversation ID to fetch videos from
469
+ max_attempts: Maximum number of polling attempts
470
+ wait_seconds: Seconds to wait between attempts
471
+ verbose: Whether to print status messages
472
+
473
+ Returns:
474
+ List of video URLs
475
+ """
476
+ import logging
477
+ logger = logging.getLogger(__name__)
478
+
479
+ # Build headers with query-specific friendly name
480
+ headers = self.build_headers(
481
+ content_type='application/x-www-form-urlencoded',
482
+ friendly_name='KadabraPromptRootQuery'
483
+ )
484
+
485
+ # Build variables
486
+ variables = {
487
+ "prompt_id": conversation_id,
488
+ "__relay_internal__pv__kadabra_voice_consumptionrelayprovider": False,
489
+ "__relay_internal__pv__AbraIsLoggedOutrelayprovider": False,
490
+ "__relay_internal__pv__KadabraConversationRenamingrelayprovider": True,
491
+ "__relay_internal__pv__KadabraSpacesEnabledrelayprovider": False,
492
+ "__relay_internal__pv__KadabraRecipesEnabledrelayprovider": False,
493
+ "__relay_internal__pv__KadabraFOASharingEnabledrelayprovider": True,
494
+ "__relay_internal__pv__KadabraFeedImageDimensionrelayprovider": 800,
495
+ "__relay_internal__pv__kadabra_story_builder_enabledrelayprovider": False,
496
+ "__relay_internal__pv__kadabra_imagine_canvas_enable_dev_settingsrelayprovider": False,
497
+ "__relay_internal__pv__enable_kadabra_partial_resultsrelayprovider": False,
498
+ "__relay_internal__pv__kadabra_create_media_deletionrelayprovider": False,
499
+ "__relay_internal__pv__kadabra_moodboardrelayprovider": False,
500
+ "__relay_internal__pv__KadabraVideoDeliveryRequestrelayprovider": {
501
+ "dash_manifest_requests": [{}],
502
+ "progressive_url_requests": [{"quality": "HD"}, {"quality": "SD"}]
503
+ },
504
+ "__relay_internal__pv__AbraSearchInlineReferencesEnabledrelayprovider": True,
505
+ "__relay_internal__pv__AbraComposedTextWidgetsrelayprovider": True,
506
+ "__relay_internal__pv__KadabraNewCitationsEnabledrelayprovider": True,
507
+ "__relay_internal__pv__WebPixelRatiorelayprovider": 1,
508
+ "__relay_internal__pv__KadabraWidgetsRedesignEnabledrelayprovider": False,
509
+ "__relay_internal__pv__AbraArtifactDragImagineFromConversationrelayprovider": True,
510
+ "__relay_internal__pv__kadabra_media_item_renderer_heightrelayprovider": 545,
511
+ "__relay_internal__pv__kadabra_media_item_renderer_widthrelayprovider": 620,
512
+ "__relay_internal__pv__AbraBugNubrelayprovider": False,
513
+ "__relay_internal__pv__AbraDebugDevOnlyrelayprovider": False,
514
+ "__relay_internal__pv__abra_silverstone_enable_hidden_commentsrelayprovider": True,
515
+ "__relay_internal__pv__kadabra_voicerelayprovider": True,
516
+ "__relay_internal__pv__KadabraSocialSearchEnabledrelayprovider": False,
517
+ "__relay_internal__pv__KadabraZeitgeistEnabledrelayprovider": False,
518
+ "__relay_internal__pv__alakazam_enabledrelayprovider": True,
519
+ "__relay_internal__pv__sp_kadabra_survey_invitationrelayprovider": True,
520
+ "__relay_internal__pv__KadabraAINativeUXrelayprovider": False,
521
+ "__relay_internal__pv__AbraArtifactsEnabledrelayprovider": True,
522
+ "__relay_internal__pv__KadabraMemoryEnabledrelayprovider": False,
523
+ "__relay_internal__pv__AbraPlannerEnabledrelayprovider": True,
524
+ "__relay_internal__pv__AbraWidgetsEnabledrelayprovider": False,
525
+ "__relay_internal__pv__KadabraDeepResearchEnabledrelayprovider": False,
526
+ "__relay_internal__pv__KadabraThinkHarderEnabledrelayprovider": False,
527
+ "__relay_internal__pv__KadabraVergeEnabledrelayprovider": False,
528
+ "__relay_internal__pv__KadabraProductSearchEnabledrelayprovider": False,
529
+ "__relay_internal__pv__KadabraAreServiceEnabledrelayprovider": False,
530
+ "__relay_internal__pv__kadabra_render_reasoning_response_statesrelayprovider": True,
531
+ "__relay_internal__pv__kadabra_reasoning_cotrelayprovider": False,
532
+ "__relay_internal__pv__kadabra_enable_send_message_retryrelayprovider": True,
533
+ "__relay_internal__pv__KadabraEmailCalendarIntegrationrelayprovider": False,
534
+ "__relay_internal__pv__kadabra_reels_connect_featuresrelayprovider": False,
535
+ "__relay_internal__pv__AbraRedteamingrelayprovider": False,
536
+ "__relay_internal__pv__kadabra_enable_open_in_editor_message_actionrelayprovider": True,
537
+ "__relay_internal__pv__AbraThreadsEnabledrelayprovider": False,
538
+ "__relay_internal__pv__AbraQPDocUploadNuxTriggerNamerelayprovider": "meta_dot_ai_abra_web_doc_upload_nux_tour",
539
+ "__relay_internal__pv__AbraSurfaceNuxIDrelayprovider": "12177"
540
+ }
541
+
542
+ # Build data payload
543
+ data = {
544
+ 'av': '813590375178585',
545
+ '__user': '0',
546
+ '__a': '1',
547
+ '__req': 's',
548
+ '__hs': '20413.HYP:kadabra_pkg.2.1...0',
549
+ 'dpr': '1',
550
+ '__ccg': 'GOOD',
551
+ '__rev': '1030219547',
552
+ '__s': 'q59jx4:9bnqdw:3ats33',
553
+ '__hsi': '7575127759957881428',
554
+ '__dyn': '7xeUjG1mxu1syUqxemh0no6u5U4e2C1vzEdE98K360CEbo1nEhw2nVEtwMw6ywaq221FwpUO0n24oaEnxO0Bo7O2l0Fwqo31w9O1lwlE-U2zxe2GewbS361qw82dUlwhE-15wmo423-0j52oS0Io5d0bS1LBwNwKG0WE8oC1IwGw-wlUcE2-G2O7E5y1rwa211wo84y1iwfe1aw',
555
+ '__csr': 'gaJNBjWsAJvliQPqlWFFknAiUB2bBjWLmhyblepaGyVFGy8y2i5pEW68mwwwPwxgtNgv2AMEu6PAgrCwc7F212xxe5YyVC1pAg01sq99uQ1zK0dp75gKzAy8y0EjcgQ8Ek0yMJC6G1og5KrXD4GexS8wdasU8U1e4075UeEuwfCA8K0hWiU2tAyE5m0gm0Jo0xUGxh1veU0gGyWfe0iK1xo32yXhoKkw56pwMw1e25onU4i0TA0xaxu00B1Q2ha2K3V0eqCmawnEgg2Gw',
556
+ '__hsdp': 'gdDdNhMlJ8bNG7i42aHgWzckH57ylAt8NkkOGCVQ8Ay8myETxW1vh48gHx-UC9Bgpy87G0BUfU7i0JFUeo7Cm12wlo5OawRwDwzxW1zg33wgodU',
557
+ '__hblp': '08W5EWt0BzUWp5Q4vz4HOk5kVogDGqmHgyi8xq9gNrxG1vh8B2K6pry4mVk8x28wuE5a1DxO1Qwr84Cu3C1VBwCxK2W2qi2y1LwDwzyK445Gwi63-0wUkxa9AyEjgogy3-',
558
+ '__sjsp': 'gdDtsAFMlJ8bNG7i47AG5lxmUmDiFQca9U',
559
+ '__comet_req': '72',
560
+ 'fb_dtsg': self.fb_dtsg,
561
+ 'jazoest': '25499',
562
+ 'lsd': self.lsd,
563
+ '__spin_r': '1030219547',
564
+ '__spin_b': 'trunk',
565
+ '__spin_t': str(int(time.time())),
566
+ '__jssesw': '1',
567
+ '__crn': 'comet.kadabra.KadabraAssistantRoute',
568
+ 'fb_api_caller_class': 'RelayModern',
569
+ 'fb_api_req_friendly_name': 'KadabraPromptRootQuery',
570
+ 'server_timestamps': 'true',
571
+ 'variables': json.dumps(variables),
572
+ 'doc_id': '25290569913909283',
573
+ }
574
+
575
+ for attempt in range(1, max_attempts + 1):
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
+
580
+ response = requests.post(
581
+ self.GRAPHQL_URL,
582
+ cookies=self.cookies,
583
+ headers=headers,
584
+ data=data,
585
+ timeout=30
586
+ )
587
+
588
+ if response.status_code == 200:
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
+ )
595
+
596
+ if video_urls:
597
+ if verbose:
598
+ logger.info(f"[VIDEO POLLING] ✓ Successfully extracted {len(video_urls)} video URL(s) on attempt {attempt}")
599
+ return video_urls
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...")
603
+ time.sleep(wait_seconds)
604
+ else:
605
+ if verbose:
606
+ logger.warning(f"[VIDEO POLLING] HTTP {response.status_code} on attempt {attempt}")
607
+ time.sleep(wait_seconds)
608
+
609
+ except Exception as e:
610
+ if verbose:
611
+ logger.error(f"[VIDEO POLLING] Error on attempt {attempt}: {e}")
612
+ time.sleep(wait_seconds)
613
+
614
+ if verbose:
615
+ logger.error(f"[VIDEO POLLING] ⚠️ Failed to extract video URLs after {max_attempts} attempts")
616
+ return []
617
+
618
+ @staticmethod
619
+ def _extract_video_urls_from_response(response_text: str, is_final_attempt: bool = False) -> List[str]:
620
+ """
621
+ Extract video URLs from Meta AI GraphQL response.
622
+ Uses the CORRECT structure from the original GitHub repo.
623
+
624
+ Args:
625
+ response_text: The response text to extract URLs from
626
+ is_final_attempt: Whether this is the final polling attempt (for logging)
627
+
628
+ Returns:
629
+ List of video URLs
630
+ """
631
+ import logging
632
+ logger = logging.getLogger(__name__)
633
+
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")
640
+
641
+ urls: List[str] = []
642
+
643
+ try:
644
+ # Parse the response
645
+ data = json.loads(response_text)
646
+ if log_details:
647
+ logger.debug("[VIDEO URL EXTRACTION] Successfully parsed response as JSON")
648
+
649
+ # CORRECT STRUCTURE from original GitHub code:
650
+ # data -> xfb_genai_fetch_post (or xab_abra__xfb_genai_fetch_post)
651
+ # -> messages -> edges -> node -> content -> imagine_video
652
+
653
+ data_obj = data.get("data", {})
654
+ fetch_post = data_obj.get("xfb_genai_fetch_post") or data_obj.get("xab_abra__xfb_genai_fetch_post") or {}
655
+
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())}")
660
+
661
+ messages = fetch_post.get("messages", {}).get("edges", [])
662
+ if log_details:
663
+ logger.debug(f"[VIDEO URL EXTRACTION] Found {len(messages)} message edges")
664
+
665
+ for edge_idx, edge in enumerate(messages):
666
+ node = edge.get("node", {})
667
+ content = node.get("content", {})
668
+ imagine_video = content.get("imagine_video") or {}
669
+
670
+ if not imagine_video:
671
+ if log_details:
672
+ logger.debug(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: No imagine_video found")
673
+ continue
674
+
675
+ if log_details:
676
+ logger.debug(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: Found imagine_video with keys: {list(imagine_video.keys())}")
677
+
678
+ # Extract from videos.nodes[] array
679
+ videos = imagine_video.get("videos", {}).get("nodes", [])
680
+ if log_details:
681
+ logger.debug(f"[VIDEO URL EXTRACTION] Edge {edge_idx}: Found {len(videos)} video nodes")
682
+
683
+ for video_idx, video in enumerate(videos):
684
+ # Try video_url or uri
685
+ uri = video.get("video_url") or video.get("uri")
686
+ if uri:
687
+ if log_details:
688
+ logger.debug(f"[VIDEO URL EXTRACTION] Found video_url/uri in videos.nodes[{video_idx}]: {uri[:100]}...")
689
+ urls.append(uri)
690
+
691
+ # Try videoDeliveryResponseResult.progressive_urls[]
692
+ delivery = video.get("videoDeliveryResponseResult") or {}
693
+ prog = delivery.get("progressive_urls", [])
694
+ if log_details and prog:
695
+ logger.debug(f"[VIDEO URL EXTRACTION] Found {len(prog)} progressive_urls in video {video_idx}")
696
+
697
+ for prog_idx, p in enumerate(prog):
698
+ pu = p.get("progressive_url")
699
+ if pu:
700
+ if log_details:
701
+ logger.debug(f"[VIDEO URL EXTRACTION] Found progressive_url[{prog_idx}]: {pu[:100]}...")
702
+ urls.append(pu)
703
+
704
+ # Extract from single video object
705
+ single_video = imagine_video.get("video") or {}
706
+ if isinstance(single_video, dict) and single_video:
707
+ if log_details:
708
+ logger.debug(f"[VIDEO URL EXTRACTION] Found single video object with keys: {list(single_video.keys())}")
709
+
710
+ uri = single_video.get("video_url") or single_video.get("uri")
711
+ if uri:
712
+ if log_details:
713
+ logger.debug(f"[VIDEO URL EXTRACTION] Found video_url/uri in single video: {uri[:100]}...")
714
+ urls.append(uri)
715
+
716
+ delivery = single_video.get("videoDeliveryResponseResult") or {}
717
+ prog = delivery.get("progressive_urls", [])
718
+ if log_details and prog:
719
+ logger.debug(f"[VIDEO URL EXTRACTION] Found {len(prog)} progressive_urls in single video")
720
+
721
+ for prog_idx, p in enumerate(prog):
722
+ pu = p.get("progressive_url")
723
+ if pu:
724
+ if log_details:
725
+ logger.debug(f"[VIDEO URL EXTRACTION] Found progressive_url[{prog_idx}]: {pu[:100]}...")
726
+ urls.append(pu)
727
+
728
+ except json.JSONDecodeError as e:
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]}")
732
+
733
+ # Fallback to regex
734
+ if log_details:
735
+ logger.debug("[VIDEO URL EXTRACTION] Falling back to regex extraction...")
736
+ import re
737
+ urls = re.findall(r'https?://[^\s"\'<>]+fbcdn[^\s"\'<>]+\.mp4[^\s"\'<>]*', response_text)
738
+ if log_details:
739
+ logger.debug(f"[VIDEO URL EXTRACTION] Regex found {len(urls)} .mp4 URLs")
740
+
741
+ # Deduplicate while preserving order
742
+ seen = set()
743
+ unique_urls: List[str] = []
744
+ for u in urls:
745
+ if u and u not in seen:
746
+ seen.add(u)
747
+ unique_urls.append(u)
748
+
749
+ # Only log final result if we have URLs or if this is the final attempt
750
+ if unique_urls:
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:
756
+ logger.warning("[VIDEO URL EXTRACTION] ⚠️ NO VIDEO URLs FOUND!")
757
+ logger.debug(f"[VIDEO URL EXTRACTION] Response preview (first 1000 chars): {response_text[:1000]}")
758
+
759
+ return unique_urls
760
+
761
+ def generate_video(
762
+ self,
763
+ prompt: str,
764
+ media_ids: Optional[List[str]] = None,
765
+ attachment_metadata: Optional[Dict[str, Any]] = None,
766
+ orientation: Optional[str] = None,
767
+ wait_before_poll: int = 10,
768
+ max_attempts: int = 30,
769
+ wait_seconds: int = 5,
770
+ verbose: bool = True
771
+ ) -> Dict:
772
+ """
773
+ Main function to generate video and retrieve URLs.
774
+
775
+ Args:
776
+ prompt: Text prompt for video generation
777
+ media_ids: Optional list of media IDs from uploaded images
778
+ attachment_metadata: Optional dict with 'file_size' (int) and 'mime_type' (str)
779
+ orientation: Video orientation ("LANDSCAPE", "VERTICAL", "SQUARE"). Defaults to "VERTICAL".
780
+ wait_before_poll: Seconds to wait before starting to poll
781
+ max_attempts: Maximum polling attempts
782
+ wait_seconds: Seconds between polling attempts
783
+ verbose: Whether to print status messages
784
+
785
+ Returns:
786
+ Dictionary with success status, conversation_id, prompt, video_urls, and timestamp
787
+ """
788
+ # Step 1: Create video generation request
789
+ conversation_id = self.create_video_generation_request(
790
+ prompt_text=prompt,
791
+ media_ids=media_ids,
792
+ attachment_metadata=attachment_metadata,
793
+ orientation=orientation,
794
+ verbose=verbose
795
+ )
796
+
797
+ if not conversation_id:
798
+ return {"success": False, "error": "Failed to create video generation request"}
799
+
800
+ # Step 2: Wait a bit before polling
801
+ time.sleep(wait_before_poll)
802
+
803
+ # Step 3: Poll for video URLs
804
+ video_urls = self.fetch_video_urls(
805
+ conversation_id=conversation_id,
806
+ max_attempts=max_attempts,
807
+ wait_seconds=wait_seconds,
808
+ verbose=verbose
809
+ )
810
+
811
+ result = {
812
+ "success": len(video_urls) > 0,
813
+ "conversation_id": conversation_id,
814
+ "prompt": prompt,
815
+ "video_urls": video_urls,
816
+ "timestamp": time.time()
817
+ }
818
+
819
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metaai-sdk
3
- Version: 2.3.3
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,10 +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_sdk-2.3.3.dist-info/licenses/LICENSE,sha256=hRLLSBixyX0tRh2k0iOGoF7nx-l-vBChNffFfVOIEtc,1290
9
- metaai_sdk-2.3.3.dist-info/METADATA,sha256=6Xudn68v_wfaznuNiH0W5UmzK1JaxTtjVe08mUN7lJo,27602
10
- metaai_sdk-2.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- metaai_sdk-2.3.3.dist-info/top_level.txt,sha256=R6YCiIQLYFKKaqhNZXDwXbpj1u01P_YhcMCVbJiDUJs,11
12
- metaai_sdk-2.3.3.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