client integration

This commit is contained in:
Brandon Thomas committed 2025-10-31 00:09:32 -04:00
1 parent 62622fc125
commit 27775bdc94
15 files changed
+321 -5

No files matched your search

@@ -1,6 +1,12 @@
/// Type for Post IDs.
/// Post IDs are UUIds.
/// 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);
impl PostId {
pub fn to_string(&self) -> String {
self.0.clone()
}
}
@@ -1,2 +1,3 @@
pub mod request_client_secrets;
mod upload_image_and_generate_video;
pub mod upload_image_and_generate_video;
pub mod upload_image_and_generate_video_with_retry;
@@ -32,7 +32,6 @@ pub struct UploadImageAndGenerateVideo<'a, P: AsRef<Path>> {
pub wait_for_generation: bool,
pub individual_request_timeout: Option<Duration>,
}
#[derive(Debug, Clone)]
@@ -0,0 +1,24 @@
use std::path::Path;
use std::time::Duration;
use crate::credentials::grok_full_credentials::GrokFullCredentials;
use crate::datatypes::file_upload_spec::FileUploadSpec;
pub struct UploadImageAndGenerateVideoWithRetry<'a, P: AsRef<Path>> {
pub credentials: &'a GrokFullCredentials,
// 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>,
}
@@ -1,6 +1,7 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use grok_client::error::grok_error::GrokError;
use midjourney_client::error::midjourney_error::MidjourneyError;
use openai_sora_client::error::sora_error::SoraError;
use storyteller_client::error::storyteller_error::StorytellerError;
@@ -53,6 +54,7 @@ pub enum BadInputReason {
#[derive(Debug)]
pub enum MissingCredentialsReason {
NeedsGrokCredentials,
NeedsFalApiKey,
NeedsMidjourneyCredentials,
NeedsMidjourneyUserId,
@@ -76,6 +78,7 @@ pub enum BillingProvider {
#[derive(Debug)]
pub enum ProviderFailureReason {
GrokError(GrokError),
//Fal(FalErrorPlus),
MidjourneyError(MidjourneyError),
/// NB: The midjourney client doesn't categorize all errors, so we have to do so on our end.
@@ -113,6 +116,10 @@ impl GenerateError {
pub fn needs_fal_api_key() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey)
}
pub fn needs_grok_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsGrokCredentials)
}
pub fn needs_midjourney_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyCredentials)
@@ -150,6 +157,12 @@ impl From<ArtcraftError> for GenerateError {
// }
//}
impl From<GrokError> for GenerateError {
fn from(value: GrokError) -> Self {
Self::ProviderFailure(ProviderFailureReason::GrokError(value))
}
}
impl From<MidjourneyError> for GenerateError {
fn from(value: MidjourneyError) -> Self {
Self::ProviderFailure(ProviderFailureReason::MidjourneyError(value))
@@ -1,6 +1,7 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_to_video::generic::handle_video::handle_video;
use crate::core::commands::enqueue::image_to_video::grok::handle_grok_video::handle_grok_video;
use crate::core::commands::enqueue::image_to_video::sora2::handle_sora2_video::handle_sora2_video;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -20,12 +21,16 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum VideoModel {
#[serde(rename = "grok")]
Grok,
#[serde(rename = "kling_1.6_pro")]
Kling16Pro,
@@ -115,6 +120,7 @@ pub async fn enqueue_image_to_video_command(
app_data_root: State<'_, AppDataRoot>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
grok_creds_manager: State<'_, GrokCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
sora_creds_manager: State<'_, SoraCredentialManager>,
@@ -129,6 +135,7 @@ pub async fn enqueue_image_to_video_command(
&app_data_root,
&provider_priority_store,
&task_database,
&grok_creds_manager,
&sora_creds_manager,
&storyteller_creds_manager,
).await;
@@ -200,11 +207,21 @@ pub async fn handle_request(
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
grok_creds_manager: &GrokCredentialManager,
sora_creds_manager: &SoraCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let result = match request.model {
Some(VideoModel::Grok) => {
handle_grok_video(
&request,
app,
app_data_root,
app_env_configs,
grok_creds_manager,
).await
}
Some(VideoModel::Sora2) => {
handle_sora2_video(
&request,
@@ -71,6 +71,11 @@ pub async fn handle_video_artcraft(
None => {
return Err(GenerateError::no_model_specified());
}
Some(VideoModel::Grok) => {
return Err(GenerateError::AnyhowError(
anyhow!("wrong logic: another branch should handle this: {:?}",
request.model)));
}
Some(VideoModel::Sora2) => {
return Err(GenerateError::AnyhowError(
anyhow!("wrong logic: another branch should handle this: {:?}",
@@ -0,0 +1,125 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::util::upload_image_media_file_to_grok::{upload_image_media_file_to_grok, UploadImageMediaFileToGrok};
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::sora::utils::upload_images_to_sora::{upload_images_to_sora, UploadImagesToSoraArgs};
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use grok_client::credentials::grok_cookies::GrokCookies;
use grok_client::credentials::grok_full_credentials::GrokFullCredentials;
use grok_client::datatypes::file_upload_spec::FileUploadSpec;
use grok_client::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
use grok_client::recipes::upload_image_and_generate_video::{upload_image_and_generate_video, UploadImageAndGenerateVideo};
use log::{error, info, warn};
use openai_sora_client::recipes::generate_sora2_video::generate_sora2_video_with_session_auto_renew::generate_sora2_video_with_session_auto_renew;
use openai_sora_client::requests::generate_sora2_video::generate_sora2_video::{GenerateSora2VideoArgs, Orientation};
use std::time::Duration;
use tauri::AppHandle;
const GROK_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_grok_video(
request: &EnqueueImageToVideoRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
grok_credential_manager: &GrokCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match grok_credential_manager.maybe_copy_full_credentials()? {
Some(creds) => creds,
None => {
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app);
return Err(GenerateError::needs_grok_credentials());
},
};
let image_token = match request.image_media_token.as_ref() {
Some(token) => token,
None => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages { min: 1, max: 1, provided: 0 }))
}
};
info!("Downloading image media file...");
let local_image_file = download_media_file_to_temp_dir(
&app_env_configs,
&app_data_root,
image_token,
).await?;
info!("Calling Grok Video generate...");
let upload = upload_image_and_generate_video(UploadImageAndGenerateVideo {
full_credentials: &creds,
file: FileUploadSpec::Path(local_image_file.path()),
prompt: request.prompt.as_deref(),
wait_for_generation: false,
individual_request_timeout: Some(GROK_IMAGE_UPLOAD_TIMEOUT)
}).await;
let post_id = match upload {
Err(err) => {
error!("Failed to use Grok video: {:?}", err);
return Err(GenerateError::from(err));
}
Ok(result) => {
info!("Successfully enqueued Grok Video: post_id = {:?}", result.post_id);
result.post_id
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Sora,
model: Some(GenerationModel::Sora2),
provider_job_id: Some(post_id.to_string()),
task_type: TaskType::VideoGeneration,
})
}
async fn get_grok_creds(app: &AppHandle, grok_credential_manager: &GrokCredentialManager) -> Result<GrokFullCredentials, GenerateError> {
if let Some(creds) = grok_credential_manager.maybe_copy_full_credentials()? {
return Ok(creds);
}
let cookies = match grok_credential_manager.maybe_copy_cookie_store()? {
Some(cookies) => cookies.to_cookie_string(),
None => {
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app);
return Err(GenerateError::needs_grok_credentials())
}
};
let cookies = GrokCookies::new(cookies);
let upgraded = request_client_secrets(RequestClientSecretsArgs {
cookies: &cookies,
}).await;
match upgraded {
Err(err) => {
error!("Failed to fetch Grok client secrets: {}", err); // NB: Fall-through
}
Ok(secrets) => {
let full_creds = GrokFullCredentials::from_cookies_and_client_secrets(cookies, secrets);
grok_credential_manager.replace_full_credentials(full_creds.clone())?;
return Ok(full_creds)
}
}
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app);
Err(GenerateError::needs_grok_credentials())
}
@@ -0,0 +1 @@
pub (super) mod handle_grok_video;
@@ -1,3 +1,4 @@
pub mod enqueue_image_to_video_command;
mod generic;
pub mod sora2;
pub mod sora2;
mod grok;
@@ -1,3 +1,4 @@
pub mod commands;
pub mod state;
pub mod util;
pub mod windows;
@@ -1,5 +1,6 @@
use cookie_store::cookie_store::CookieStore;
use grok_client::credentials::grok_client_secrets::GrokClientSecrets;
use grok_client::credentials::grok_full_credentials::GrokFullCredentials;
use grok_client::credentials::grok_user_data::GrokUserData;
#[derive(Clone)]
@@ -9,6 +10,10 @@ pub struct GrokCredentialHolder {
/// The Grok client consumes a string-only form (rather than this cookie jar)
pub browser_cookies: Option<CookieStore>,
/// Full credentials.
/// NOT PERSISTED TO DISK.
pub grok_full_credentials: Option<GrokFullCredentials>,
/// Email, user id, etc.
/// These can be persisted.
pub grok_user_data: Option<GrokUserData>,
@@ -23,6 +28,7 @@ impl GrokCredentialHolder {
pub fn empty() -> Self {
Self {
browser_cookies: None,
grok_full_credentials: None,
grok_user_data: None,
grok_client_secrets: None,
}
@@ -4,6 +4,8 @@ use crate::services::grok::state::grok_serializable_state::{GrokSerializableStat
use cookie_store::cookie_store::CookieStore;
use errors::AnyhowResult;
use grok_client::credentials::grok_client_secrets::GrokClientSecrets;
use grok_client::credentials::grok_cookies::GrokCookies;
use grok_client::credentials::grok_full_credentials::GrokFullCredentials;
use grok_client::credentials::grok_user_data::GrokUserData;
use log::{info, warn};
use std::fs::read_to_string;
@@ -50,6 +52,7 @@ impl GrokCredentialManager {
credential_data = Arc::new(RwLock::new(GrokCredentialHolder {
browser_cookies: maybe_cookies,
grok_user_data: user_data,
grok_full_credentials: None, // NB: We don't want to keep this on disk. It goes stale.
grok_client_secrets: None, // NB: We don't want to keep this on disk. It goes stale.
}));
}
@@ -68,6 +71,29 @@ impl GrokCredentialManager {
}
}
pub fn maybe_copy_cookie_header_string(&self) -> anyhow::Result<Option<String>> {
let maybe_cookies = self.maybe_copy_cookie_store()?;
let maybe_cookies = maybe_cookies.map(|cookies| {
cookies.to_cookie_string()
});
Ok(maybe_cookies)
}
pub fn maybe_copy_typed_cookies(&self) -> anyhow::Result<Option<GrokCookies>> {
let maybe_cookies = self.maybe_copy_cookie_header_string()?;
let maybe_cookies = maybe_cookies.map(|cookies| {
GrokCookies::new(cookies)
});
Ok(maybe_cookies)
}
pub fn maybe_copy_full_credentials(&self) -> anyhow::Result<Option<GrokFullCredentials>> {
match self.credential_data.read() {
Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(holder) => Ok(holder.grok_full_credentials.clone()),
}
}
pub fn maybe_copy_client_secrets(&self) -> anyhow::Result<Option<GrokClientSecrets>> {
match self.credential_data.read() {
Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)),
@@ -85,6 +111,16 @@ impl GrokCredentialManager {
}
}
pub fn replace_full_credentials(&self, creds: GrokFullCredentials) -> anyhow::Result<()> {
match self.credential_data.write() {
Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut holder) => {
holder.grok_full_credentials = Some(creds);
Ok(())
}
}
}
pub fn replace_client_secrets(&self, secrets: GrokClientSecrets) -> anyhow::Result<()> {
match self.credential_data.write() {
Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)),
@@ -0,0 +1 @@
pub mod upload_image_media_file_to_grok;
@@ -0,0 +1,80 @@
use crate::core::state::app_env_configs::app_env_configs_serializeable::StorytellerApiHost;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use errors::AnyhowResult;
use grok_client::datatypes::api::file_id::FileId;
use grok_client::datatypes::file_upload_spec::FileUploadSpec;
use grok_client::requests::upload_file::grok_upload_file::GrokUploadFile;
use log::{error, info};
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use std::time::Duration;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::endpoints::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tokens::tokens::media_files::MediaFileToken;
const GROK_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub struct UploadImageMediaFileToGrok<'a> {
pub storyteller_host: &'a ApiHost,
pub image_media_token: &'a MediaFileToken,
pub app_data_root: &'a AppDataRoot,
pub grok_creds_manager: &'a GrokCredentialManager,
}
pub struct UploadImageMediaFileToGrokResult {
pub grok_file_id: FileId,
}
pub async fn upload_image_media_file_to_grok(
args: UploadImageMediaFileToGrok<'_>,
) -> AnyhowResult<UploadImageMediaFileToGrokResult> {
info!("Calling get media file API: {:?}", args.storyteller_host);
info!("Using media token: {:?}", args.image_media_token);
let response = get_media_file(
args.storyteller_host,
args.image_media_token
).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
let filename = args.app_data_root.downloads_dir().path().join(&filename);
simple_http_download(&media_file_url, &filename).await?;
let cookies = args.grok_creds_manager.maybe_copy_cookie_header_string()?
.ok_or_else(|| anyhow!("Missing Grok cookies"))?;
info!("Uploading image to Grok...");
let upload = GrokUploadFile {
file: FileUploadSpec::Path(filename),
cookie: cookies,
request_timeout: Some(GROK_IMAGE_UPLOAD_TIMEOUT),
};
let response = upload.upload().await?;
let file_id = response.file_id
.ok_or_else(|| anyhow!("Media upload did not produce a file_id!"))?;
Ok(UploadImageMediaFileToGrokResult {
grok_file_id: file_id,
})
}