This commit is contained in:
Brandon Thomas committed 2025-10-30 19:51:43 -04:00
1 parent 53e9f1c463
commit 893a29f965
8 files changed
+140 -37

No files matched your search

@@ -1,5 +1,6 @@
pub mod baggage;
pub mod file_id;
pub mod post_id;
pub mod request_id;
pub mod sentry_trace;
pub mod svg_path_data;
@@ -0,0 +1,6 @@
/// Type for Post IDs.
/// Post IDs are UUIds.
/// These are often shared with media/image uploads' `FileId`s, but seldom video generation `FileId`s
#[derive(Clone, Debug)]
pub struct PostId(pub String);
@@ -1,5 +1,6 @@
use crate::credentials::grok_full_credentials::GrokFullCredentials;
use crate::datatypes::api::file_id::FileId;
use crate::datatypes::api::post_id::PostId;
use crate::datatypes::file_upload_spec::FileUploadSpec;
use crate::error::grok_error::GrokError;
use crate::error::grok_generic_api_error::GrokGenericApiError;
@@ -19,17 +20,45 @@ pub struct UploadImageAndGenerateVideo<'a, P: AsRef<Path>> {
// NB: Must be owned.
pub file: FileUploadSpec<P>,
/// Video generation prompt
pub prompt: Option<&'a str>,
/// Wait for the full video to be generated before returning
/// By default, the endpoint stays open for 30-ish seconds while
/// the video generates and streams back JSON progress updates.
/// If we set this to false, we'll wait for the video ID and exit
/// the request early, before the video is finished asynchronously
/// generating.
pub wait_for_generation: bool,
pub individual_request_timeout: Option<Duration>,
}
pub struct ImageUploadResult {
#[derive(Debug, Clone)]
pub struct ImageUploadAndGenerateVideoResult {
/// The ID of the "post"
/// This often is equivalent to the upload id / `image_file_id`,
/// and this is in the user-facing URL for website navigation to the video.
pub post_id: PostId,
/// The ID of the file we uploaded
pub image_file_id: FileId,
/// The ID of the video we're generating, which may or may not exist yet.
pub video_file_id: Option<FileId>,
/// The URL for the video, which may or may not exist yet.
pub video_url: Option<String>,
/// Whether the video generation has already completed.
/// Based on request `wait_for_generation`.
pub generation_is_complete: bool,
}
pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAndGenerateVideo<'_, P>) -> Result<ImageUploadResult, GrokError> {
pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAndGenerateVideo<'_, P>)
-> Result<ImageUploadAndGenerateVideoResult, GrokError>
{
info!("Uploading file to Grok...");
@@ -44,7 +73,7 @@ pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAn
info!("File URI: {:?}", upload_result.file_uri);
info!("File Metadata ID: {:?}", upload_result.file_id);
let upload_file_id = match &upload_result.file_id {
let upload_file_id = match upload_result.file_id {
Some(id) => id,
None => {
error!("Previous file upload failed. Cannot continue.");
@@ -63,13 +92,13 @@ pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAn
let request = GrokCreateMediaPost {
user_id: &args.full_credentials.client_secrets.user_id,
file_id: upload_file_id,
file_id: &upload_file_id,
media_type: MediaPostType::UserUploadedImage,
cookie: args.full_credentials.cookies.as_str(),
request_timeout: args.individual_request_timeout,
};
let _post_result = request.send().await?;
let post_result = request.send().await?;
info!("Posted Image.");
@@ -89,9 +118,20 @@ pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAn
numbers: &args.full_credentials.client_secrets.numbers,
};
let video_gen_result = request.stream_only_video_id().await?;
let maybe_video_file_id;
let generation_is_complete;
let maybe_video_file_id = video_gen_result.video_file_id;
if args.wait_for_generation {
info!("Waiting for video generation...");
let video_gen_result = request.send().await?;
maybe_video_file_id = video_gen_result.video_file_id;
generation_is_complete = true;
} else {
info!("Streaming response partially...");
let video_gen_result = request.stream_only_video_id().await?;
maybe_video_file_id = video_gen_result.video_file_id;
generation_is_complete = false;
}
let maybe_video_url = maybe_video_file_id
.as_ref()
@@ -118,9 +158,12 @@ pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAn
info!("Media Liked");
Ok(ImageUploadResult {
Ok(ImageUploadAndGenerateVideoResult {
post_id: post_result.post_id,
image_file_id: upload_file_id,
video_file_id: maybe_video_file_id,
video_url: maybe_video_url,
generation_is_complete,
})
}
@@ -145,23 +188,23 @@ mod tests {
async fn test_upload_image_and_generate_video() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Info);
//let image_path = "/Users/bt/Pictures/Zelda 64 Art/FCgYX6tWEAEhpsy.jpg";
//let prompt = "our hero link plunges the sword into the pedestal, the temple is glowing with a blue aura";
let image_path = "/Users/bt/Pictures/Midjourney/hipster_girl.png";
let maybe_prompt = Some("an anime girl runs away from a giant t-rex");
let cookies = get_typed_test_cookies()?;
let secrets = request_client_secrets(RequestClientSecretsArgs {
cookies: &cookies,
}).await?;
//let image_path = "/Users/bt/Pictures/Zelda 64 Art/FCgYX6tWEAEhpsy.jpg";
//let prompt = "our hero link plunges the sword into the pedestal, the temple is glowing with a blue aura";
let image_path = "/Users/bt/Pictures/Wallpaper/digital_topography_midjourney.png";
let maybe_prompt = Some("the mountains are digitial and lights flicker up and down the topography. the camera pans gently by");
println!("Verification Token: {:?}", secrets.verification_token);
println!("Sentry Trace: {:?}", secrets.sentry_trace);
println!("Numbers: {:?}", secrets.numbers);
println!("Svg Path: {:?}", secrets.svg_path_data);
println!("Baggage: {:?}", secrets.baggage);
println!("[test] Verification Token: {:?}", secrets.verification_token);
println!("[test] Sentry Trace: {:?}", secrets.sentry_trace);
println!("[test] Numbers: {:?}", secrets.numbers);
println!("[test] Svg Path: {:?}", secrets.svg_path_data);
println!("[test] Baggage: {:?}", secrets.baggage);
let credentials = GrokFullCredentials::from_cookies_and_client_secrets(cookies, secrets);
@@ -170,10 +213,13 @@ mod tests {
file: FileUploadSpec::Path(image_path),
prompt: maybe_prompt,
individual_request_timeout: None,
wait_for_generation: false,
}).await?;
println!("Video File ID: {:?}", video_result.video_file_id);
println!("Video URL: {:?}", video_result.video_url);
println!("[test] Post ID: {:?}", video_result.post_id);
println!("[test] Image File ID: {:?}", video_result.image_file_id);
println!("[test] Video File ID: {:?}", video_result.video_file_id);
println!("[test] Video URL: {:?}", video_result.video_url);
assert_eq!(1, 2);
@@ -0,0 +1,13 @@
{
"post": {
"id": "3c896963-8e6b-4e71-83e1-4253b2bf2ac8",
"userId":"85980643-ffab-4984-a3de-59a608c47d7f",
"createTime":"2025-10-30T23:22:19.528702688Z",
"prompt":"",
"mediaType":"MEDIA_POST_TYPE_IMAGE",
"mediaUrl":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/3c896963-8e6b-4e71-83e1-4253b2bf2ac8/content",
"mimeType":"image/jpeg",
"audioUrls":[],
"childPosts":[]
}
}
@@ -1,10 +1,12 @@
use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT;
use crate::datatypes::api::file_id::FileId;
use crate::datatypes::api::post_id::PostId;
use crate::datatypes::api::user_id::UserId;
use crate::error::grok_client_error::GrokClientError;
use crate::error::grok_error::GrokError;
use crate::error::grok_generic_api_error::GrokGenericApiError;
use crate::requests::create_media_post::request::CreateMediaPostWireRequest;
use crate::requests::create_media_post::response::CreateMediaPostRawResponse;
use crate::requests::upload_file::grok_upload_file::{GrokUploadFile, GrokUploadFileResponse};
use crate::utils::user_and_file_id_to_image_url::user_and_file_id_to_image_url;
use log::{error, info};
@@ -32,8 +34,9 @@ pub enum MediaPostType {
}
/// Response type
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct GrokCreateMediaPostResponse {
pub post_id: PostId,
}
impl <'a> GrokCreateMediaPost<'a> {
@@ -47,9 +50,17 @@ impl <'a> GrokCreateMediaPost<'a> {
// TODO: Headers were from Chromium, not Firefox. Partial implementation.
let mut request_builder = client.post(CREATE_MEDIA_POST_URL)
// TODO: Missing header "baggage"
// TODO: Missing header "sentry-trace"
// TODO: Missing header "traceparent"
// TODO: Missing header "traceparent"
// TODO: Missing header "x-statsig-id"
// TODO: Missing header "x-xai-request-id"
//.header("sec-ch-ua", "") // NB: Not present in firefox
//.header("sec-ch-ua-mobile", "") // NB: Not present in firefox
//.header("sec-ch-ua-platform", "") // NB: Not present in firefox
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
// TODO: Missing header "baggage"
.header(CACHE_CONTROL, "no-cache")
.header(CONTENT_TYPE, "application/json")
.header(COOKIE, self.cookie.to_string())
@@ -57,17 +68,9 @@ impl <'a> GrokCreateMediaPost<'a> {
.header(PRAGMA, "no-cache")
.header("priority", "u=1, i")
.header(REFERER, "https://grok.com/imagine/favorites")
//.header("sec-ch-ua", "") // TODO
//.header("sec-ch-ua-mobile", "") // TODO
//.header("sec-ch-ua-platform", "") // TODO
.header("sec-fetch-dest", "empty")
.header("sec-fetch-mode", "cors")
.header("sec-fetch-site", "same-origin")
// TODO: Missing header "sentry-trace"
// TODO: Missing header "traceparent"
// TODO: Missing header "traceparent"
// TODO: Missing header "x-statsig-id"
// TODO: Missing header "x-xai-request-id"
.header(USER_AGENT, FIREFOX_143_MAC_USER_AGENT);
//info!("Sending...");
@@ -106,23 +109,24 @@ impl <'a> GrokCreateMediaPost<'a> {
let status = response.status();
let response_body = &response.text()
let response_body = response.text()
.await
.map_err(|err| {
error!("Error reading Grok create media response body: {:?}", err);
GrokGenericApiError::WreqError(err)
})?;
// TODO:
// TODO: Handle errors (Cloudflare, Grok, etc.)
if !status.is_success() {
error!("Upload file request returned an error (code {}) : {:?}", status.as_u16(), response_body);
//return Err(classify_general_http_status_code_and_body(status, response_body));
}
//let response : GrokApiUploadFileResponse = serde_json::from_str(response_body)
// .map_err(|err| GrokGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
let response : CreateMediaPostRawResponse = serde_json::from_str(&response_body)
.map_err(|err| GrokGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
Ok(GrokCreateMediaPostResponse{
Ok(GrokCreateMediaPostResponse {
post_id: PostId(response.post.id),
})
}
}
@@ -153,6 +157,8 @@ mod tests {
let result = request.send().await?;
println!("Result: {:?}", result);
println!("Post ID: {:?}", result.post_id);
assert_eq!(1, 2);
Ok(())
}
@@ -1,2 +1,3 @@
pub mod grok_create_media_post;
pub (super) mod request;
pub (super) mod request;
mod response;
@@ -0,0 +1,28 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CreateMediaPostRawResponse {
pub post: PostData,
}
#[derive(Deserialize)]
pub struct PostData {
/// The identifier for the post
pub id: String,
}
/*
{
"post": {
"id": "3c896963-8e6b-4e71-83e1-4253b2bf2ac8",
"userId":"85980643-ffab-4984-a3de-59a608c47d7f",
"createTime":"2025-10-30T23:22:19.528702688Z",
"prompt":"",
"mediaType":"MEDIA_POST_TYPE_IMAGE",
"mediaUrl":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/3c896963-8e6b-4e71-83e1-4253b2bf2ac8/content",
"mimeType":"image/jpeg",
"audioUrls":[],
"childPosts":[]
}
}
*/
@@ -110,6 +110,7 @@ impl <'a> GrokVideoGenChatConversationBuilder<'a> {
})
}
pub async fn send(&self) -> Result<GrokVideoGenChatConversationResponse, GrokError> {
info!("Configuring client and request...");
let client = Self::build_client()?;
@@ -164,6 +165,7 @@ impl <'a> GrokVideoGenChatConversationBuilder<'a> {
})
}
fn build_client() -> Result<Client, GrokClientError> {
Ok(Client::builder()
.emulation(Emulation::Firefox143)