mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-09 03:21:23 +00:00
upload file wip
This commit is contained in:
1 parent
515f2b9c28
commit
a85a5693f9
10 files changed
+342
No files matched your search
Generated
+1
@@ -4673,6 +4673,7 @@ name = "grok_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"cloudflare_mitigation",
|
||||
|
||||
@@ -23,6 +23,7 @@ errors.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -19,6 +19,15 @@ pub enum GrokClientError {
|
||||
/// Error sending to an open websocket.
|
||||
WebsocketSendError(wreq::Error),
|
||||
|
||||
/// Can't open a local file for uploading.
|
||||
CannotOpenLocalFileForUpload(std::io::Error),
|
||||
|
||||
/// Can't read a local file for uploading.
|
||||
CannotReadLocalFileForUpload(std::io::Error),
|
||||
|
||||
/// File for upload has an invalid path.
|
||||
FileForUploadHasInvalidPath,
|
||||
|
||||
// /// An error reading the file for upload.
|
||||
// FileForUploadReadError(std::io::Error),
|
||||
//
|
||||
@@ -66,6 +75,9 @@ impl Display for GrokClientError {
|
||||
Self::WebsocketLockError => write!(f, "Websocket lock error"),
|
||||
Self::WebsocketReadError(err) => write!(f, "Websocket read error: {}", err),
|
||||
Self::WebsocketSendError(err) => write!(f, "Websocket send error: {}", err),
|
||||
Self::CannotOpenLocalFileForUpload(err) => write!(f, "Cannot open local file for upload: {}", err),
|
||||
Self::CannotReadLocalFileForUpload(err) => write!(f, "Cannot read local file for upload: {}", err),
|
||||
Self::FileForUploadHasInvalidPath => write!(f, "File for upload has invalid path"),
|
||||
|
||||
//Self::FileForUploadReadError(err) => write!(f, "Error reading file for upload: {}", err),
|
||||
//Self::FileForUploadHasInvalidPath => write!(f, "The file path provided for upload is invalid."),
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
Upload Image -> Video Flow
|
||||
==========================
|
||||
|
||||
1. Upload Image
|
||||
|
||||
Endpoint: https://grok.com/rest/app-chat/upload-file
|
||||
|
||||
Response Fields:
|
||||
{
|
||||
**"fileMetadataId": "21a79085-e206-4b0b-88ac-5f2b7a453e45",**
|
||||
**"fileUri": "users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content",**
|
||||
"fileMimeType": "image/jpeg",
|
||||
"fileName": "0_0.jpeg",
|
||||
"parsedFileUri": "",
|
||||
"createTime": "2025-10-21T23:40:41.784448Z",
|
||||
"fileSource": "SELF_UPLOAD_FILE_SOURCE"
|
||||
}
|
||||
|
||||
|
||||
File becomes a 480x640 copy:
|
||||
|
||||
https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content
|
||||
|
||||
2. "Create Media Post" **(needed?)**
|
||||
|
||||
Endpoint: https://grok.com/rest/media/post/create
|
||||
|
||||
Request Fields:
|
||||
{
|
||||
"mediaType": "MEDIA_POST_TYPE_IMAGE",
|
||||
"mediaUrl": "https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"post": {
|
||||
"id": "21a79085-e206-4b0b-88ac-5f2b7a453e45",
|
||||
"userId": "85980643-ffab-4984-a3de-59a608c47d7f",
|
||||
"createTime": "2025-10-21T23:40:41.993980477Z",
|
||||
"prompt": "",
|
||||
"mediaType": "MEDIA_POST_TYPE_IMAGE",
|
||||
"mediaUrl": "https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content",
|
||||
"mimeType": "image/jpeg",
|
||||
"audioUrls": [],
|
||||
"childPosts": []
|
||||
}
|
||||
}
|
||||
|
||||
3. Chat Conversation for Video
|
||||
|
||||
Endpoint: https://grok.com/rest/app-chat/conversations/new
|
||||
|
||||
Request:
|
||||
{
|
||||
"temporary":true,
|
||||
"modelName":"grok-3",
|
||||
"message":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content --mode=normal",
|
||||
"fileAttachments":["21a79085-e206-4b0b-88ac-5f2b7a453e45"],
|
||||
"toolOverrides": {
|
||||
"videoGen":true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Generate Image -> Video Flow
|
||||
============================
|
||||
|
||||
(TODO)
|
||||
|
||||
3. Chat Conversation for Video
|
||||
|
||||
Endpoint: https://grok.com/rest/app-chat/conversations/new
|
||||
|
||||
Request:
|
||||
{
|
||||
"temporary": true,
|
||||
"modelName": "grok-3",
|
||||
"message": "https://imagine-public.x.ai/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png alien walks down the hallway --mode=custom",
|
||||
"toolOverrides": {
|
||||
"videoGen":true
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
It looks like this requests generates the video and opens a long-lived, long-polling endpoint
|
||||
that delivers lots of JSON blobs, eventually that contain the final video ID.
|
||||
|
||||
Before this is called, the "create media post" endpoint is called.
|
||||
@@ -0,0 +1,189 @@
|
||||
use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT;
|
||||
use crate::error::grok_client_error::GrokClientError;
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::requests::upload_file::grok_upload_file_response::GrokUploadFileResponse;
|
||||
use crate::requests::upload_file::request::UploadFileRequest;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use log::{error, info};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONTENT_TYPE, COOKIE, ORIGIN, PRAGMA, REFERER, USER_AGENT};
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
const UPLOAD_FILE_URL : &str = "https://grok.com/rest/app-chat/upload-file";
|
||||
|
||||
/// Try to prevent buffer reallocations.
|
||||
/// There's a better way to implement this.
|
||||
const INITIAL_BUFFER_SIZE : usize = 1024*1024;
|
||||
|
||||
pub struct GrokUploadFile<'a, P: AsRef<Path>> {
|
||||
pub file: FileSpec<'a, P>,
|
||||
pub cookie: String,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub enum FileSpec<'a, P: AsRef<Path>> {
|
||||
/// Path to the file
|
||||
Path(P),
|
||||
|
||||
/// Bytes of the file
|
||||
Bytes {
|
||||
/// Must be owned.
|
||||
bytes: &'a [u8],
|
||||
/// Must be owned.
|
||||
filename: String,
|
||||
/// Mime type
|
||||
mimetype: String,
|
||||
}
|
||||
}
|
||||
|
||||
impl <'a, P> GrokUploadFile<'a, P> where P: AsRef<Path> {
|
||||
|
||||
pub async fn upload(&self) -> Result<GrokUploadFileResponse, GrokError> {
|
||||
match &self.file {
|
||||
FileSpec::Path(path) => {
|
||||
self.upload_from_file(path.as_ref()).await
|
||||
}
|
||||
FileSpec::Bytes { bytes, filename, mimetype } => {
|
||||
unimplemented!("todo: implement bytes upload")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn upload_from_file(&self, file_path: &Path) -> Result<GrokUploadFileResponse, GrokError> {
|
||||
let mut file = File::open(file_path)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to open file for upload: {}", err);
|
||||
GrokClientError::CannotOpenLocalFileForUpload(err)
|
||||
})?;
|
||||
|
||||
let mut buffer = Vec::with_capacity(INITIAL_BUFFER_SIZE);
|
||||
|
||||
file.read_to_end(&mut buffer)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to read file for upload: {}", err);
|
||||
GrokClientError::CannotReadLocalFileForUpload(err)
|
||||
})?;
|
||||
|
||||
let filename = file_path
|
||||
.file_name()
|
||||
.ok_or_else(|| GrokClientError::FileForUploadHasInvalidPath)?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let maybe_ext = file_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str());
|
||||
|
||||
// TODO: Share library code.
|
||||
// TODO: Read file magic bytes first, then fall back to this.
|
||||
let mime_type = match maybe_ext {
|
||||
Some("jpg") | Some("jpeg") => "image/jpeg",
|
||||
Some("png") => "image/png",
|
||||
// Some("webp") => "image/webp",
|
||||
// Some("gif") => "image/gif",
|
||||
// Some("mp4") => "video/mp4",
|
||||
// Some("mov") => "video/quicktime",
|
||||
// Some("webm") => "video/webm",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
|
||||
// The encoded images have '/' and '+'.
|
||||
// TODO: Padding or no?
|
||||
let base64_file = BASE64_STANDARD.encode(buffer);
|
||||
|
||||
self.do_upload(&base64_file, &filename, &mime_type ).await
|
||||
}
|
||||
|
||||
async fn do_upload(&self, base64_file: &str, file_name: &str, mime_type: &str) -> Result<GrokUploadFileResponse, GrokError> {
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox143)
|
||||
.build()
|
||||
.map_err(|err| GrokClientError::WreqClientError(err))?;
|
||||
|
||||
info!("Configuring client...");
|
||||
|
||||
// TODO: Headers were from Chromium, not Firefox. Partial implementation.
|
||||
let mut request_builder = client.post(UPLOAD_FILE_URL)
|
||||
.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())
|
||||
.header(ORIGIN, "https://grok.com")
|
||||
.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...");
|
||||
//let response = builder.send()
|
||||
// .await
|
||||
// .map_err(|err| GrokClientError::WreqClientError(err))?;
|
||||
|
||||
if let Some(timeout) = self.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
/*
|
||||
let request_body = UploadFileRequest {
|
||||
fileName: file_name.to_string(),
|
||||
fileMimeType: mime_type.to_string(),
|
||||
content: base64_file,
|
||||
fileSource: "IMAGINE_SELF_UPLOAD_FILE_SOURCE".to_string(),
|
||||
};
|
||||
|
||||
let http_request = request_builder.json(&request_body)
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
error!("Error building Sora image generation HTTP request: {:?}", err);
|
||||
SoraClientError::WreqClientError(err)
|
||||
})?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error during Sora image generation request: {:?}", err);
|
||||
SoraClientError::WreqClientError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = &response.text().await
|
||||
.map_err(|err| {
|
||||
error!("Error reading Sora image generation response body: {:?}", err);
|
||||
SoraClientError::WreqClientError(err)
|
||||
})?;
|
||||
|
||||
if !status.is_success() {
|
||||
error!("Sora image generation request returned an error (code {}) : {:?}", status.as_u16(), response_body);
|
||||
return Err(classify_general_http_status_code_and_body(status, response_body));
|
||||
}
|
||||
|
||||
let response : HttpCreateResponse = serde_json::from_str(response_body)
|
||||
.map_err(|err| SoraGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
*/
|
||||
|
||||
unimplemented!("todo")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/// Internal response structure
|
||||
pub struct GrokUploadFileResponse {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod grok_upload_file;
|
||||
mod grok_upload_file_response;
|
||||
mod request;
|
||||
@@ -0,0 +1,18 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub (super) struct UploadFileRequest {
|
||||
#[serde(rename="fileName")]
|
||||
pub fileName: String,
|
||||
|
||||
#[serde(rename="fileMimeType")]
|
||||
pub fileMimeType: String,
|
||||
|
||||
/// Base64-encoded content
|
||||
pub content: String,
|
||||
|
||||
/// eg. 'IMAGINE_SELF_UPLOAD_FILE_SOURCE'
|
||||
#[serde(rename="fileSource")]
|
||||
pub fileSource: String,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::fs::write;
|
||||
use base64::Engine;
|
||||
use base64::prelude::{BASE64_STANDARD, BASE64_STANDARD_NO_PAD};
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[test]
|
||||
fn test_pad() -> AnyhowResult<()> {
|
||||
let buffer = get_base64_file();
|
||||
let base64_file = BASE64_STANDARD.decode(buffer)?;
|
||||
write("file_pad.jpg", &base64_file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_pad() -> AnyhowResult<()> {
|
||||
let buffer = get_base64_file();
|
||||
let base64_file = BASE64_STANDARD_NO_PAD.decode(buffer)?;
|
||||
write("file_no_pad.jpg", &base64_file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_base64_file() -> &'static str {
|
||||
"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
Line truncated
|
||||
}
|
||||
Reference in new issue
Block a user