mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 11:19:29 -04:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57976d2d7 | ||
|
|
ec26e76f62 | ||
|
|
9ec1358594 | ||
|
|
b53b281485 | ||
|
|
c0596a2a3e | ||
|
|
99cd4ac725 | ||
|
|
dcbd67c803 | ||
|
|
4b901a9ea2 | ||
|
|
64930fa3ce | ||
|
|
92577699dd | ||
|
|
931618055f | ||
|
|
97bd706973 | ||
|
|
521d6de991 |
Generated
+10
@@ -3601,6 +3601,7 @@ dependencies = [
|
||||
"log",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"test_data",
|
||||
"testing",
|
||||
"tokio",
|
||||
"url",
|
||||
@@ -10853,6 +10854,15 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
|
||||
|
||||
[[package]]
|
||||
name = "test_data"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"errors",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "testing"
|
||||
version = "0.0.1"
|
||||
|
||||
@@ -76,6 +76,7 @@ members = [
|
||||
"crates/lib/sqlx_mysql_helpers",
|
||||
"crates/lib/storyteller_root",
|
||||
"crates/lib/subprocess_common",
|
||||
"crates/lib/test_data",
|
||||
"crates/lib/testing",
|
||||
"crates/lib/tts_common",
|
||||
"crates/lib/url_config",
|
||||
@@ -232,5 +233,6 @@ primitives = { path = "crates/lib/data/primitives" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
storyteller_client = { path = "crates/api_clients/storyteller_client" }
|
||||
subprocess_common = { path = "crates/lib/subprocess_common" }
|
||||
test_data = { path = "crates/lib/test_data" }
|
||||
tokens = { path = "crates/schema/public/tokens" }
|
||||
web_base64 = { path = "crates/lib/data/web_base64" }
|
||||
|
||||
@@ -16,8 +16,8 @@ path = "src/lib.rs"
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
file_to_base64_url.workspace = true
|
||||
errors.workspace = true
|
||||
file_to_base64_url.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
@@ -35,3 +35,4 @@ fal = { path = "../../vendor/fal-rs", features = ["endpoints"] }
|
||||
|
||||
[dev-dependencies]
|
||||
testing = { path = "../../lib/testing" }
|
||||
test_data.workspace = true
|
||||
|
||||
@@ -8,6 +8,7 @@ impl FalApiKey {
|
||||
}
|
||||
|
||||
pub fn from_str(api_key: &str) -> Self {
|
||||
Self(api_key.to_string())
|
||||
let api_key = api_key.trim().to_string();
|
||||
Self(api_key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod fal_api_key;
|
||||
pub mod fal_api_key;
|
||||
pub mod open_ai_api_key;
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
/// This is for Fal's OpenAI "Bring Your Own Key" (BYOK) endpoints.
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAiApiKey(pub String);
|
||||
|
||||
impl OpenAiApiKey {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self(api_key)
|
||||
}
|
||||
|
||||
pub fn from_str(api_key: &str) -> Self {
|
||||
let api_key = api_key.trim().to_string();
|
||||
Self(api_key)
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::gpt_image_1::edit_image::byok::{gpt_edit_image, GptEditImageRequest};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct GptEditImageByokArgs<'a, V: IntoUrl> {
|
||||
// Request
|
||||
pub image_urls: Vec<String>,
|
||||
pub prompt: &'a str,
|
||||
pub image_size: GptEditImageSize,
|
||||
pub num_images: GptEditImageNumImages,
|
||||
pub quality: GptEditImageQuality,
|
||||
|
||||
// Fulfillment
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub openai_api_key: &'a OpenAiApiKey,
|
||||
pub webhook_url: V,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptEditImageSize{
|
||||
Auto,
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptEditImageQuality {
|
||||
Auto,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptEditImageNumImages{
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_gpt_image_1_edit_image_webhook<V: IntoUrl>(
|
||||
args: GptEditImageByokArgs<'_, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
// auto, 1024x1024, 1536x1024, 1024x1536
|
||||
let image_size = match args.image_size {
|
||||
GptEditImageSize::Auto => "auto",
|
||||
GptEditImageSize::Square => "1024x1024",
|
||||
GptEditImageSize::Horizontal => "1536x1024",
|
||||
GptEditImageSize::Vertical => "1024x1536",
|
||||
};
|
||||
|
||||
let quality = match args.quality {
|
||||
GptEditImageQuality::Auto => "auto",
|
||||
GptEditImageQuality::Low => "low",
|
||||
GptEditImageQuality::Medium => "medium",
|
||||
GptEditImageQuality::High => "high",
|
||||
};
|
||||
|
||||
let num_images = match args.num_images {
|
||||
GptEditImageNumImages::One => 1,
|
||||
GptEditImageNumImages::Two => 2,
|
||||
GptEditImageNumImages::Three => 3,
|
||||
GptEditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = GptEditImageRequest {
|
||||
image_urls: args.image_urls,
|
||||
prompt: args.prompt.to_string(),
|
||||
image_size: image_size.to_string(),
|
||||
num_images,
|
||||
quality: quality.to_string(),
|
||||
openai_api_key: args.openai_api_key.0.to_string(),
|
||||
};
|
||||
|
||||
let result = gpt_edit_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::{enqueue_gpt_image_1_edit_image_webhook, GptEditImageByokArgs, GptEditImageNumImages, GptEditImageQuality, GptEditImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, GRASSY_HILL_TRANSPARENT_IMAGE_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
|
||||
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
|
||||
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let fal_api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/openai_api_key.txt")?;
|
||||
|
||||
let open_ai_api_key = OpenAiApiKey::from_str(&secret);
|
||||
|
||||
let args = GptEditImageByokArgs {
|
||||
image_urls: vec![
|
||||
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
GRASSY_HILL_TRANSPARENT_IMAGE_URL.to_string(),
|
||||
],
|
||||
prompt: "put the man and the ghost on the grassy hill. the man is scared of the friendly ghost.",
|
||||
api_key: &fal_api_key,
|
||||
openai_api_key: &open_ai_api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
image_size: GptEditImageSize::Horizontal,
|
||||
num_images: GptEditImageNumImages::One,
|
||||
quality: GptEditImageQuality::High,
|
||||
};
|
||||
|
||||
let result = enqueue_gpt_image_1_edit_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ pub mod enqueue_flux_1_dev_text_to_image_webhook;
|
||||
pub mod enqueue_flux_1_schnell_text_to_image_webhook;
|
||||
pub mod enqueue_flux_pro_11_text_to_image_webhook;
|
||||
pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
|
||||
pub mod remove_background_rembg_webhook;
|
||||
pub mod enqueue_gpt_image_1_edit_image_webhook;
|
||||
pub mod remove_background_rembg_webhook;
|
||||
|
||||
-2
@@ -55,8 +55,6 @@ mod tests {
|
||||
|
||||
println!("{:?}", response);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageRequest, GptImage1EditImageResponse, GPT_IMAGE_1_EDIT_IMAGE_PATH};
|
||||
|
||||
|
||||
pub async fn gpt_image_1_edit_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GptImage1EditImageRequest,
|
||||
) -> Result<GptImage1EditImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GPT_IMAGE_1_EDIT_IMAGE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod gpt_image_1_edit_image;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod edit;
|
||||
pub mod generate_flux_1_dev_text_to_image;
|
||||
pub mod generate_flux_1_schnell_text_to_image;
|
||||
pub mod generate_flux_pro_11_text_to_image;
|
||||
|
||||
@@ -41,9 +41,9 @@ pub async fn get_media_file(api_host: &ApiHost, media_file_token: &MediaFileToke
|
||||
}
|
||||
|
||||
fn get_media_file_token_route(api_host: &ApiHost, media_file_token: &MediaFileToken) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
let api_hostname_and_scheme = api_host.to_api_hostname_and_scheme();
|
||||
let media_file_token = media_file_token.as_str();
|
||||
format!("https://{}/v1/media_files/file/{}", api_hostname, media_file_token)
|
||||
format!("{}/v1/media_files/file/{}", api_hostname_and_scheme, media_file_token)
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-22): Share API definitions between client and server in common crate.
|
||||
|
||||
+2
-2
@@ -84,8 +84,8 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}/v1/media_files/upload/image", api_hostname)
|
||||
let api_hostname_and_scheme = api_host.to_api_hostname_and_scheme();
|
||||
format!("{}/v1/media_files/upload/image", api_hostname_and_scheme)
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-22): Share API definitions between client and server in common crate.
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, EnqueueTextToImageSuccessResponse};
|
||||
use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_artcraft;
|
||||
use crate::core::commands::enqueue::image::handle_image_fal::handle_image_fal;
|
||||
use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
|
||||
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
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::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowError;
|
||||
use log::{error, info, warn};
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
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 openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EnqueueContextualEditImageCommand {
|
||||
/// The model to use.
|
||||
pub model: Option<ContextualImageEditModel>,
|
||||
|
||||
/// Images to use for the image edit.
|
||||
/// The first image is typically a 2D canvas or 3D stage, but doesn't have to be.
|
||||
/// There must be at least one image.
|
||||
pub image_media_tokens: Vec<MediaFileToken>,
|
||||
|
||||
/// The user's image generation prompt.
|
||||
pub prompt: String,
|
||||
|
||||
/// Turn off the system prompt.
|
||||
pub disable_system_prompt: Option<bool>,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub image_count: Option<u32>,
|
||||
|
||||
/// Aspect ratio.
|
||||
pub aspect_ratio: Option<EditImageSize>,
|
||||
|
||||
/// Image quality.
|
||||
pub image_quality: Option<EditImageQuality>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EditImageSize {
|
||||
Auto,
|
||||
Square,
|
||||
Wide,
|
||||
Tall,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EditImageQuality {
|
||||
Auto,
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EnqueueContextualEditImageErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
|
||||
/// No model available for image generation
|
||||
NoProviderAvailable,
|
||||
|
||||
/// Generic bad request error
|
||||
BadRequest,
|
||||
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
|
||||
/// The user is sending too many requests
|
||||
TooManyConcurrentTasks,
|
||||
/// User is not logged into Sora!
|
||||
SoraLoginRequired,
|
||||
/// The user needs to create a Sora account
|
||||
SoraUsernameNotYetCreated,
|
||||
/// The Sora service is having problems. Try again soon.
|
||||
SoraIsHavingProblems,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EnqueueContextualEditImageSuccessResponse {
|
||||
}
|
||||
|
||||
impl SerializeMarker for EnqueueContextualEditImageSuccessResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enqueue_contextual_edit_image_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueContextualEditImageCommand,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
) -> ResponseOrErrorType<EnqueueContextualEditImageSuccessResponse, EnqueueContextualEditImageErrorType> {
|
||||
|
||||
info!("enqueue_contextual_edit_image_command called; image media tokens : {:?}, full request: {:?}",
|
||||
&request.image_media_tokens, &request);
|
||||
|
||||
let result = handle_request(
|
||||
&request,
|
||||
&app,
|
||||
&app_data_root,
|
||||
&app_env_configs,
|
||||
&provider_priority_store,
|
||||
&storyteller_creds_manager,
|
||||
&fal_creds_manager,
|
||||
&fal_task_queue,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("Error enqueuing contextual edit image: {:?}", err);
|
||||
|
||||
// TODO: Derive from err. Make service provider optional.
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Artcraft, // FIXME: This is wrong.
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Err(err.to_tauri_response())
|
||||
}
|
||||
Ok(event) => {
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: event.service_provider,
|
||||
model: Some(event.tauri_event_model()),
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Ok(EnqueueContextualEditImageSuccessResponse {}.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
|
||||
match request.model {
|
||||
None => {
|
||||
Err(InternalContextualEditImageError::NoModelSpecified)
|
||||
}
|
||||
Some(ContextualImageEditModel::GptImage1) => {
|
||||
handle_gpt_image_1(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await
|
||||
}
|
||||
// TODO(bt,2025-07-05): Flux Kontext, etc.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageErrorType;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use errors::AnyhowError;
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use serde::Serialize;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InternalContextualEditImageError {
|
||||
/// Request did not specify a model.
|
||||
NoModelSpecified,
|
||||
|
||||
/// No provider is configured for the model type.
|
||||
NoProviderAvailable,
|
||||
|
||||
/// No storyteller credentials are set.
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
/// An invalid number of images was requested.
|
||||
InvalidNumberOfRequestedImages {
|
||||
min: u32,
|
||||
max: u32,
|
||||
requested: u32,
|
||||
},
|
||||
|
||||
/// An invalid number of input images was requested.
|
||||
InvalidNumberOfInputImages {
|
||||
min: u32,
|
||||
max: u32,
|
||||
requested: u32,
|
||||
},
|
||||
|
||||
/// No sora credentials are set.
|
||||
NeedsSoraCredentials,
|
||||
|
||||
SoraError(SoraError),
|
||||
AnyhowError(AnyhowError),
|
||||
StorytellerError(StorytellerError),
|
||||
}
|
||||
|
||||
impl From<AnyhowError> for InternalContextualEditImageError {
|
||||
fn from(value: AnyhowError) -> Self {
|
||||
Self::AnyhowError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SoraError> for InternalContextualEditImageError {
|
||||
fn from(value: SoraError) -> Self {
|
||||
Self::SoraError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StorytellerError> for InternalContextualEditImageError {
|
||||
fn from(value: StorytellerError) -> Self {
|
||||
Self::StorytellerError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl InternalContextualEditImageError {
|
||||
pub fn to_tauri_response<E: Serialize>(&self) -> CommandErrorResponseWrapper<EnqueueContextualEditImageErrorType, E> {
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = EnqueueContextualEditImageErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string();
|
||||
|
||||
match self {
|
||||
InternalContextualEditImageError::NoModelSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueContextualEditImageErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation".to_string();
|
||||
}
|
||||
InternalContextualEditImageError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueContextualEditImageErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for image generation".to_string();
|
||||
}
|
||||
InternalContextualEditImageError::InvalidNumberOfRequestedImages { min, max, requested } => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueContextualEditImageErrorType::BadRequest;
|
||||
error_message = format!("Invalid number of images requested ({}). Must be between {} and {}", requested, min, max);
|
||||
|
||||
}
|
||||
InternalContextualEditImageError::InvalidNumberOfInputImages { min, max, requested } => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueContextualEditImageErrorType::BadRequest;
|
||||
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", requested, min, max);
|
||||
}
|
||||
InternalContextualEditImageError::SoraError(_) => {}
|
||||
InternalContextualEditImageError::AnyhowError(_) => {}
|
||||
InternalContextualEditImageError::StorytellerError(_) => {}
|
||||
InternalContextualEditImageError::NeedsStorytellerCredentials => {}
|
||||
InternalContextualEditImageError::NeedsSoraCredentials => {}
|
||||
}
|
||||
|
||||
CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
|
||||
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::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
// TODO: Check if providers are available before proceeding.
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {
|
||||
// Fallthrough
|
||||
// If in the future we support OpenAI API keys, it's worth considering whether we
|
||||
// send those requests ourselves, or if we use FAL as an intermediary. FAL makes
|
||||
// the API nicer to deal with, but the user needs an additional key.
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
return handle_gpt_image_1_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager
|
||||
).await;
|
||||
}
|
||||
Provider::Sora => {
|
||||
return handle_gpt_image_1_sora(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalContextualEditImageError::NoProviderAvailable)
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
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::ProviderPriorityStore;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1_artcraft(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(InternalContextualEditImageError::NeedsStorytellerCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft gpt-image-1 ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let image_quality = request.image_quality
|
||||
.map(|quality| match quality {
|
||||
EditImageQuality::Auto => GptImage1EditImageImageQuality::Auto,
|
||||
EditImageQuality::High => GptImage1EditImageImageQuality::High,
|
||||
EditImageQuality::Medium => GptImage1EditImageImageQuality::Medium,
|
||||
EditImageQuality::Low => GptImage1EditImageImageQuality::Low,
|
||||
});
|
||||
|
||||
let image_size = request.aspect_ratio
|
||||
.map(|size| match size {
|
||||
EditImageSize::Auto => GptImage1EditImageImageSize::Square,
|
||||
EditImageSize::Square => GptImage1EditImageImageSize::Square,
|
||||
EditImageSize::Tall => GptImage1EditImageImageSize::Vertical,
|
||||
EditImageSize::Wide => GptImage1EditImageImageSize::Horizontal,
|
||||
});
|
||||
|
||||
let num_images = match request.image_count {
|
||||
None => None,
|
||||
Some(1) => Some(GptImage1EditImageNumImages::One),
|
||||
Some(2) => Some(GptImage1EditImageNumImages::Two),
|
||||
Some(3) => Some(GptImage1EditImageNumImages::Three),
|
||||
Some(4) => Some(GptImage1EditImageNumImages::Four),
|
||||
Some(other) => {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfRequestedImages {
|
||||
min: 1,
|
||||
max: 4,
|
||||
requested: other,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
let request = GptImage1EditImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: Some(request.prompt.clone()),
|
||||
image_media_tokens: Some(request.image_media_tokens.clone()),
|
||||
image_size,
|
||||
num_images,
|
||||
image_quality,
|
||||
};
|
||||
|
||||
let result = gpt_image_1_edit_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(enqueued) => {
|
||||
// TODO(bt,2025-07-05): Enqueue job token?
|
||||
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
|
||||
enqueued.inference_job_token);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
|
||||
return Err(InternalContextualEditImageError::StorytellerError(err));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ContextualEditImageSuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Artcraft,
|
||||
model: ContextualImageEditModel::GptImage1,
|
||||
})
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
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::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
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 openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use std::time::Duration;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use tauri::AppHandle;
|
||||
|
||||
const SORA_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
|
||||
|
||||
const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
|
||||
|
||||
const DEFAULT_ASPECT_RATIO : EditImageSize = EditImageSize::Square;
|
||||
|
||||
pub async fn handle_gpt_image_1_sora(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
|
||||
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
|
||||
|
||||
let sora_creds = match sora_creds_manager.get_credentials() {
|
||||
Ok(Some(creds)) => creds,
|
||||
Ok(None) => {
|
||||
warn!("No Sora credentials found.");
|
||||
return Err(InternalContextualEditImageError::NeedsSoraCredentials);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to get Sora credentials: {:?}", err);
|
||||
return Err(InternalContextualEditImageError::AnyhowError(err));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(bt,2025-07-08): Handle many images.
|
||||
// TODO(bt,2025-07-08): Handle authenticated sessions (for private images).
|
||||
// TODO(bt,2025-07-08): Handle HTTP batch get requests.
|
||||
let first_image_token = request.image_media_tokens.first()
|
||||
.ok_or_else(|| InternalContextualEditImageError::InvalidNumberOfInputImages {
|
||||
min: 1,
|
||||
max: 10, // TODO Confirm
|
||||
requested: request.image_media_tokens.len() as u32,
|
||||
})?;
|
||||
|
||||
info!("Calling get media file API: {:?}", app_env_configs.storyteller_host);
|
||||
|
||||
let response = get_media_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
first_image_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 = app_data_root.downloads_dir().path().join(&filename);
|
||||
|
||||
simple_http_download(&media_file_url, &filename).await?;
|
||||
|
||||
let files_to_upload = vec![filename];
|
||||
|
||||
let mut creds = sora_creds_manager.get_credentials_required()?;
|
||||
|
||||
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upgrade or renew session: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
if credential_updated {
|
||||
info!("Storing updated credentials");
|
||||
sora_creds_manager.set_credentials(&creds)?;
|
||||
}
|
||||
|
||||
let mut sora_media_tokens = Vec::with_capacity(files_to_upload.len());
|
||||
|
||||
for (i, file_path) in files_to_upload.iter().enumerate() {
|
||||
info!("Uploading image {} of {}...", (i+1), files_to_upload.len());
|
||||
|
||||
let (response, maybe_new_credentials) =
|
||||
image_upload_from_file_with_session_auto_renew(ImageUploadFromFileAutoRenewRequest {
|
||||
file_path,
|
||||
credentials: &creds,
|
||||
request_timeout: Some(SORA_IMAGE_UPLOAD_TIMEOUT), // TODO: Centralize and make configurable.
|
||||
}).await?;
|
||||
|
||||
if let Some(new_creds) = maybe_new_credentials {
|
||||
info!("Storing updated credentials.");
|
||||
sora_creds_manager.set_credentials(&new_creds)?;
|
||||
creds = new_creds;
|
||||
}
|
||||
|
||||
sora_media_tokens.push(response.id);
|
||||
}
|
||||
|
||||
let aspect_ratio = request.aspect_ratio.unwrap_or(DEFAULT_ASPECT_RATIO);
|
||||
|
||||
let aspect_ratio = match aspect_ratio {
|
||||
EditImageSize::Auto => ImageSize::Square,
|
||||
EditImageSize::Square => ImageSize::Square,
|
||||
EditImageSize::Wide => ImageSize::Wide,
|
||||
EditImageSize::Tall => ImageSize::Tall,
|
||||
};
|
||||
|
||||
info!("Calling Sora image generation...");
|
||||
|
||||
// TODO(bt,2025-04-21): Download media tokens.
|
||||
// Note: This is incredibly inefficient. We should keep a local cache.
|
||||
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
|
||||
|
||||
let (response, maybe_new_creds) =
|
||||
image_remix_with_session_auto_renew(ImageRemixAutoRenewRequest {
|
||||
prompt: request.prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: aspect_ratio,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: &creds,
|
||||
request_timeout: Some(SORA_IMAGE_REMIX_TIMEOUT),
|
||||
}).await?;
|
||||
|
||||
if let Some(new_creds) = maybe_new_creds {
|
||||
info!("Storing updated credentials.");
|
||||
sora_creds_manager.set_credentials(&new_creds)?;
|
||||
}
|
||||
|
||||
info!("New Sora Task ID: {:?} ", response.task_id);
|
||||
|
||||
sora_task_queue.insert(&response.task_id)?;
|
||||
|
||||
Ok(ContextualEditImageSuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Sora,
|
||||
model: ContextualImageEditModel::GptImage1,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod handle_gpt_image_1;
|
||||
pub mod handle_gpt_image_1_artcraft;
|
||||
pub mod handle_gpt_image_1_sora;
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod enqueue_contextual_edit_image_command;
|
||||
pub mod errors;
|
||||
pub mod gpt_image_1;
|
||||
pub mod success_event;
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::core::events::generation_events::common::{GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
|
||||
pub struct ContextualEditImageSuccessEvent {
|
||||
pub service_provider: GenerationServiceProvider,
|
||||
pub model: ContextualImageEditModel,
|
||||
}
|
||||
|
||||
impl ContextualEditImageSuccessEvent {
|
||||
pub fn tauri_event_model(&self) -> GenerationModel {
|
||||
match self.model {
|
||||
ContextualImageEditModel::GptImage1 => GenerationModel::GptImage1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod image;
|
||||
pub mod image_edit;
|
||||
pub mod object;
|
||||
pub mod video;
|
||||
+19
-4
@@ -15,6 +15,7 @@ use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnqueueImageTo3dObjectRequest {
|
||||
@@ -65,6 +66,7 @@ pub enum EnqueueImageTo3dObjectErrorType {
|
||||
pub async fn enqueue_image_to_3d_object_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
@@ -76,8 +78,9 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
info!("enqueue_image_to_3d_object_command called");
|
||||
|
||||
let result = handle_request(
|
||||
&app,
|
||||
request,
|
||||
&app,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
@@ -132,8 +135,9 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
|
||||
|
||||
pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
@@ -148,12 +152,23 @@ pub async fn handle_request(
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_object_artcraft(
|
||||
request, &app, app_data_root, storyteller_creds_manager).await?);
|
||||
request,
|
||||
app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
storyteller_creds_manager
|
||||
).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_object_fal(
|
||||
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
|
||||
&app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
request,
|
||||
fal_creds_manager,
|
||||
fal_task_queue
|
||||
).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
|
||||
@@ -13,12 +14,12 @@ use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
|
||||
use storyteller_client::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_object_artcraft(
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<(), InternalObjectError> {
|
||||
@@ -64,7 +65,7 @@ pub async fn handle_object_artcraft(
|
||||
media_file_token: request.image_media_token,
|
||||
};
|
||||
let result = generate_hunyuan3d_2_0_image_to_3d(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
@@ -86,7 +87,7 @@ pub async fn handle_object_artcraft(
|
||||
media_file_token: request.image_media_token,
|
||||
};
|
||||
let result = generate_hunyuan3d_2_1_image_to_3d(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
@@ -16,6 +17,7 @@ use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_object_fal(
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
@@ -46,8 +48,11 @@ pub async fn handle_object_fal(
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
|
||||
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else {
|
||||
return Err(InternalObjectError::AnyhowError(anyhow!("No image media token provided")));
|
||||
}
|
||||
|
||||
+19
-4
@@ -5,6 +5,7 @@ use crate::core::commands::response::failure_response_wrapper::{CommandErrorResp
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::model::video_models::VideoModel;
|
||||
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::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
@@ -54,6 +55,7 @@ pub enum EnqueueImageToVideoErrorType {
|
||||
pub async fn enqueue_image_to_video_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
@@ -65,8 +67,9 @@ pub async fn enqueue_image_to_video_command(
|
||||
info!("enqueue_image_to_video_command called");
|
||||
|
||||
let result = handle_request(
|
||||
&app,
|
||||
request,
|
||||
&app,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
@@ -121,8 +124,9 @@ pub async fn enqueue_image_to_video_command(
|
||||
|
||||
|
||||
pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
@@ -137,12 +141,23 @@ pub async fn handle_request(
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_video_artcraft(
|
||||
request, &app, app_data_root, storyteller_creds_manager).await?);
|
||||
request,
|
||||
&app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
storyteller_creds_manager
|
||||
).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_video_fal(
|
||||
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
|
||||
&app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
request,
|
||||
fal_creds_manager,
|
||||
fal_task_queue
|
||||
).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::model::video_models::VideoModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProAspectRatio, GenerateKling16ProImageToVideoRequest};
|
||||
@@ -28,6 +29,7 @@ use tauri::AppHandle;
|
||||
pub async fn handle_video_artcraft(
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<(), InternalVideoError> {
|
||||
@@ -76,7 +78,7 @@ pub async fn handle_video_artcraft(
|
||||
duration: None,
|
||||
};
|
||||
let result = generate_kling_16_pro_image_to_video(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
@@ -102,7 +104,7 @@ pub async fn handle_video_artcraft(
|
||||
duration: None,
|
||||
};
|
||||
let result = generate_kling_21_master_image_to_video(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
@@ -128,7 +130,7 @@ pub async fn handle_video_artcraft(
|
||||
duration: None,
|
||||
};
|
||||
let result = generate_kling_21_pro_image_to_video(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
@@ -154,7 +156,7 @@ pub async fn handle_video_artcraft(
|
||||
duration: None,
|
||||
};
|
||||
let result = generate_seedance_1_0_lite_image_to_video(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
@@ -180,7 +182,7 @@ pub async fn handle_video_artcraft(
|
||||
duration: None,
|
||||
};
|
||||
let result = generate_veo_2_image_to_video(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::model::video_models::VideoModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
@@ -17,6 +18,7 @@ use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_video_fal(
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
@@ -47,8 +49,11 @@ pub async fn handle_video_fal(
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
|
||||
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else {
|
||||
return Err(InternalVideoError::AnyhowError(anyhow!("No image media token provided")));
|
||||
}
|
||||
|
||||
@@ -22,9 +22,18 @@ use crate::core::commands::response::success_response_wrapper::CommandSuccessRes
|
||||
// - sora_image_remix_command
|
||||
|
||||
/// Easy to use infallible response.
|
||||
/// This always contains a success payload and never results in an error.
|
||||
pub type InfallibleResponse<SuccessPayload> = CommandSuccessResponseWrapper<SuccessPayload>;
|
||||
|
||||
/// Easy to use Result<T, E> type.
|
||||
///
|
||||
/// There are easier ways to use Response<S, Et, Ep> :
|
||||
/// - `SimpleResponse` just returns strings on success or error (but provides clues to the frontend)
|
||||
/// - `SuccessOrErrorMessage` returns an empty success payload or an error message
|
||||
/// - `ResponseOrErrorMessage<SuccessPayload>` returns a success payload or an error message
|
||||
/// - `ResponseOrErrorType<SuccessPayload, ErrType>` returns a success payload or an error type
|
||||
/// - `ResponseOrError<SuccessPayload, ErrPayload>` returns a success payload or an error payload
|
||||
///
|
||||
pub type Response<SuccessPayload, ErrType, ErrPayload> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ErrPayload>>;
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
/// 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 ContextualImageEditModel {
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
|
||||
// TODO: Flux Kontext models.
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod contextual_image_edit_models;
|
||||
pub mod image_models;
|
||||
pub mod video_models;
|
||||
@@ -1,16 +1,18 @@
|
||||
use crate::core::artcraft_error::ArtcraftError;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
// TODO: Better more concrete error handling
|
||||
|
||||
pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> Result<NamedTempFile, ArtcraftError> {
|
||||
let response = get_media_file(&ApiHost::Storyteller, token).await?;
|
||||
pub async fn download_media_file_to_temp_dir(app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, token: &MediaFileToken) -> Result<NamedTempFile, ArtcraftError> {
|
||||
let response = get_media_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
token
|
||||
).await?;
|
||||
|
||||
let media_file_url = &response.media_file.media_links.cdn_url;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use tauri::Manager;
|
||||
use crate::core::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
|
||||
use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::enqueue_text_to_image_command;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::enqueue_contextual_edit_image_command;
|
||||
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command;
|
||||
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
|
||||
use crate::core::commands::flip_image::flip_image;
|
||||
@@ -81,6 +82,8 @@ pub fn run() {
|
||||
let app_env_configs = AppEnvConfigs::load_from_filesystem(&app_data_root)
|
||||
.expect("AppEnvConfigs should be loaded from disk");
|
||||
|
||||
let app_env_configs_2 = app_env_configs.clone();
|
||||
|
||||
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(&app_data_root) {
|
||||
Ok(Some(priority)) => {
|
||||
println!("Loaded provider priority from disk: {:?}", priority.get_priority());
|
||||
@@ -134,6 +137,7 @@ pub fn run() {
|
||||
let app_3 = app.clone();
|
||||
let app_4 = app.clone();
|
||||
let app_5 = app.clone();
|
||||
let app_env_configs_3 = app_env_configs_2.clone();
|
||||
let app_data_root_3 = app_data_root_2.clone();
|
||||
let app_data_root_4 = app_data_root_2.clone();
|
||||
let app_data_root_5 = app_data_root_2.clone();
|
||||
@@ -172,9 +176,9 @@ pub fn run() {
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_env_configs_2, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
|
||||
tauri::async_runtime::spawn(discord_presence_thread());
|
||||
tauri::async_runtime::spawn(fal_task_polling_thread(app_5, app_data_root_5, fal_creds_manager_3, storyteller_creds_manager_4, fal_task_queue_3));
|
||||
tauri::async_runtime::spawn(fal_task_polling_thread(app_5, app_env_configs_3, app_data_root_5, fal_creds_manager_3, fal_task_queue_3, storyteller_creds_manager_4));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -189,6 +193,7 @@ pub fn run() {
|
||||
.manage(storyteller_creds_manager_3)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
check_sora_session_command,
|
||||
enqueue_contextual_edit_image_command,
|
||||
enqueue_image_to_3d_object_command,
|
||||
enqueue_image_to_video_command,
|
||||
enqueue_text_to_image_command,
|
||||
@@ -200,10 +205,10 @@ pub fn run() {
|
||||
get_app_preferences_command,
|
||||
get_fal_api_key_command,
|
||||
get_provider_order_command,
|
||||
set_provider_order_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
set_fal_api_key_command,
|
||||
set_provider_order_command,
|
||||
sora_image_generation_command,
|
||||
sora_image_remix_command,
|
||||
sora_logout_command,
|
||||
|
||||
+16
-6
@@ -7,6 +7,7 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
@@ -68,6 +69,7 @@ pub enum BackgroundRemovalErrorType {
|
||||
pub async fn fal_background_removal_command(
|
||||
app: AppHandle,
|
||||
request: FalBackgroundRemovalRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
@@ -107,7 +109,8 @@ pub async fn fal_background_removal_command(
|
||||
}
|
||||
|
||||
let result = remove_background(
|
||||
request,
|
||||
request,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager
|
||||
@@ -202,6 +205,7 @@ impl From<ArtcraftError> for InnerError {
|
||||
|
||||
pub async fn remove_background(
|
||||
request: FalBackgroundRemovalRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
@@ -214,8 +218,11 @@ pub async fn remove_background(
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
info!("From media token: {:?}", &media_token);
|
||||
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
|
||||
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else if let Some(base64_bytes) = request.base64_image {
|
||||
info!("From base64 bytes...");
|
||||
temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?;
|
||||
@@ -250,15 +257,18 @@ pub async fn remove_background(
|
||||
info!("Uploading image media file...");
|
||||
|
||||
let upload_result = upload_image_media_file_from_file(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
result_filename
|
||||
result_filename,
|
||||
).await?;
|
||||
|
||||
// TODO: Don't re-request to simply build MediaLinks (or CDN URL). Get those from the upload API in one turn.
|
||||
info!("Re-requesting media file...");
|
||||
|
||||
let response = get_media_file(&ApiHost::Storyteller, &upload_result.media_file_token).await?;
|
||||
let response = get_media_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
&upload_result.media_file_token,
|
||||
).await?;
|
||||
|
||||
info!("Uploaded media file: {:?}", response.media_file.token);
|
||||
|
||||
|
||||
+9
-2
@@ -6,6 +6,7 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
@@ -47,6 +48,7 @@ pub enum HunyuanErrorType {
|
||||
pub async fn fal_hunyuan_image_to_3d_command(
|
||||
app: AppHandle,
|
||||
request: FalHunyuanImageTo3dRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
@@ -71,6 +73,7 @@ pub async fn fal_hunyuan_image_to_3d_command(
|
||||
|
||||
let result = image_to_3d(
|
||||
request,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
@@ -171,6 +174,7 @@ impl From<ArtcraftError> for InnerError {
|
||||
|
||||
pub async fn image_to_3d(
|
||||
request: FalHunyuanImageTo3dRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
@@ -182,8 +186,11 @@ pub async fn image_to_3d(
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
|
||||
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else if let Some(base64_bytes) = request.base64_image {
|
||||
temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?;
|
||||
} else {
|
||||
|
||||
+9
-1
@@ -6,6 +6,7 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
@@ -52,6 +53,7 @@ pub enum KlingImageToVideoErrorType {
|
||||
pub async fn fal_kling_image_to_video_command(
|
||||
app: AppHandle,
|
||||
request: FalKlingImageToVideoRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
@@ -76,6 +78,7 @@ pub async fn fal_kling_image_to_video_command(
|
||||
|
||||
let result = image_to_video(
|
||||
request,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
@@ -175,6 +178,7 @@ impl From<ArtcraftError> for InnerError {
|
||||
|
||||
pub async fn image_to_video(
|
||||
request: FalKlingImageToVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
@@ -187,7 +191,11 @@ pub async fn image_to_video(
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
|
||||
} else if let Some(base64_bytes) = request.base64_image {
|
||||
temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?;
|
||||
|
||||
@@ -15,20 +15,23 @@ use storyteller_client::recipes::upload_media_file_from_file::upload_media_file_
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
pub async fn fal_task_polling_thread(
|
||||
app_handle: AppHandle,
|
||||
app_env_configs: AppEnvConfigs,
|
||||
app_data_root: AppDataRoot,
|
||||
fal_creds_manager: FalCredentialManager,
|
||||
storyteller_creds_manager: StorytellerCredentialManager,
|
||||
fal_task_queue: FalTaskQueue,
|
||||
storyteller_creds_manager: StorytellerCredentialManager,
|
||||
) -> ! {
|
||||
loop {
|
||||
let res = polling_loop(
|
||||
&app_handle,
|
||||
&app_env_configs,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
&storyteller_creds_manager,
|
||||
&app_data_root,
|
||||
).await;
|
||||
if let Err(err) = res {
|
||||
@@ -40,9 +43,10 @@ pub async fn fal_task_polling_thread(
|
||||
|
||||
async fn polling_loop(
|
||||
app_handle: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_data_root: &AppDataRoot,
|
||||
) -> AnyhowResult<()> {
|
||||
loop {
|
||||
@@ -119,7 +123,7 @@ async fn polling_loop(
|
||||
let download_file = download_url_to_temp_dir(&url, &app_data_root).await?;
|
||||
|
||||
let result = upload_media_file_from_file(
|
||||
&ApiHost::Storyteller,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&storyteller_creds),
|
||||
download_file
|
||||
).await;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
@@ -22,7 +23,6 @@ use serde_derive::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
@@ -79,6 +79,7 @@ pub enum SoraImageRemixErrorType {
|
||||
pub async fn sora_image_remix_command(
|
||||
app: AppHandle,
|
||||
request: SoraImageRemixCommand,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
@@ -117,7 +118,13 @@ pub async fn sora_image_remix_command(
|
||||
});
|
||||
}
|
||||
|
||||
let result = generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue).await;
|
||||
let result = generate_image(
|
||||
request,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
@@ -207,12 +214,16 @@ impl From<StorytellerError> for InnerError {
|
||||
|
||||
pub async fn generate_image(
|
||||
request: SoraImageRemixCommand,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<(), InnerError> {
|
||||
|
||||
let response = get_media_file(&ApiHost::Storyteller, &request.snapshot_media_token).await?;
|
||||
let response = get_media_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
&request.snapshot_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)
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent;
|
||||
use crate::core::events::generation_events::generation_failed_event::GenerationFailedEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
@@ -17,12 +18,12 @@ use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::AppHandle;
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub async fn sora_task_polling_thread(
|
||||
app_handle: AppHandle,
|
||||
app_env_configs: AppEnvConfigs,
|
||||
app_data_root: AppDataRoot,
|
||||
sora_creds_manager: SoraCredentialManager,
|
||||
storyteller_creds_manager: StorytellerCredentialManager,
|
||||
@@ -31,6 +32,7 @@ pub async fn sora_task_polling_thread(
|
||||
loop {
|
||||
let res = polling_loop(
|
||||
&app_handle,
|
||||
&app_env_configs,
|
||||
&sora_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&sora_task_queue,
|
||||
@@ -45,6 +47,7 @@ pub async fn sora_task_polling_thread(
|
||||
|
||||
async fn polling_loop(
|
||||
app_handle: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
@@ -119,7 +122,6 @@ async fn polling_loop(
|
||||
sora_task_queue.remove_list(&failed_task_ids)?;
|
||||
|
||||
let creds = storyteller_creds_manager.get_credentials_required()?;
|
||||
let api_host = ApiHost::Storyteller;
|
||||
|
||||
for task in succeeded_tasks.iter() {
|
||||
if !sora_task_queue.contains_key(&task.id)? {
|
||||
@@ -132,7 +134,11 @@ async fn polling_loop(
|
||||
let download_path = download_generation(generation, &app_data_root).await?;
|
||||
|
||||
info!("Uploading to backend...");
|
||||
let result = upload_image_media_file_from_file(&api_host, Some(&creds), download_path).await?;
|
||||
let result = upload_image_media_file_from_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
download_path
|
||||
).await?;
|
||||
|
||||
info!("Uploaded to API backend: {:?}", result.media_file_token);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main"],"permissions":["core:default","log:default"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -18,12 +18,12 @@ normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# NB: This is one of the most central crates in the monorepo and everything depends on it.
|
||||
# Consider the dependencies here wisely.
|
||||
# Ideally we have almost nothing as a dependency of this crate.
|
||||
anyhow = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "test_data"
|
||||
edition = "2021"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
# NB: Did not look for public-facing crate with same name.
|
||||
name = "test_data"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
@@ -0,0 +1,18 @@
|
||||
//! A library with test data for our test cases.
|
||||
|
||||
// Never allow these
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
|
||||
|
||||
// Okay to toggle
|
||||
#![forbid(unreachable_patterns)]
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
// Always allow
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod web;
|
||||
@@ -0,0 +1,25 @@
|
||||
//! Links to images that should remain available for tests to download.
|
||||
|
||||
// media token: m_g978j1h93fatjastnv43bcwnem667p
|
||||
pub const ERNEST_GHOST_TREX_IMAGE_URL :&str = "https://cdn-2.fakeyou.com/media/h/p/v/2/n/hpv2ncy2d3wcy02mr2kh9e807z8whrm8/image_hpv2ncy2d3wcy02mr2kh9e807z8whrm8.png";
|
||||
|
||||
// media token: m_1997908e4tcbaj34wa0s5bvr4y4szt
|
||||
pub const ERNEST_SCARED_STUPID_IMAGE_URL :&str = "https://cdn-2.fakeyou.com/media/t/8/r/q/3/t8rq3b8tvabpyrmp9c0tw9t612bjq4rm/asset_t8rq3b8tvabpyrmp9c0tw9t612bjq4rm.jpg";
|
||||
|
||||
// media token: m_f5yy0fdmkwrf2hxaq4yw4sd95tvvrf
|
||||
pub const FOREST_BACKDROP_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/0/p/0/s/9/0p0s9brjd8k3xr6qgahmkkhs5d4kga30/asset_0p0s9brjd8k3xr6qgahmkkhs5d4kga30.jpg";
|
||||
|
||||
// media token: m_deprc7e171byqqe0z8n9f7tjqxvdvj
|
||||
pub const GHOST_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/g/r/m/e/t/grmetywfpztmxcn2f796ambvm3hdhrt3/asset_grmetywfpztmxcn2f796ambvm3hdhrt3.jpg";
|
||||
|
||||
// media token: m_6mhxhv9wat9p63nc6w502zxrz31t3a
|
||||
pub const GRASSY_HILL_TRANSPARENT_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/y/4/n/w/7/y4nw7rebh54j4cnw7nmcy81v2nsjkpxg/asset_y4nw7rebh54j4cnw7nmcy81v2nsjkpxg.png";
|
||||
|
||||
// media token: m_43vfk9rc35yt2rxb1tgcsx11wedc01
|
||||
pub const MOUNTAIN_TREE_IMAGE_URL :&str = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
|
||||
|
||||
// media token: m_xmzdeqhjpxs5418kvepcrj5a3vkfb1
|
||||
pub const WHITE_HOUSE_SUNSET_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/q/t/z/w/k/qtzwka8dd0g2nfyk94k1qh3zzfwqtfhn/asset_qtzwka8dd0g2nfyk94k1qh3zzfwqtfhn.jpg";
|
||||
|
||||
// media token: m_z61yg5w7dnnvhb1f9sqt8jf02y61bv
|
||||
pub const WIDE_FALL_MOUNTAINS_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/w/6/m/j/p/w6mjpcqkky7f2f3xkyvkcv3qeedkmejs/asset_w6mjpcqkky7f2f3xkyvkcv3qeedkmejs.jpg";
|
||||
@@ -0,0 +1 @@
|
||||
pub mod image_urls;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GPT_IMAGE_1_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/gpt_image_1";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GptImage1EditImageRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Text prompt to generate the image from.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
// TODO: OneOf for image_urls or image_media_tokens
|
||||
/// Image media tokens to include in the editing context.
|
||||
pub image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Size of the image to generate. Default is Square.
|
||||
pub image_size: Option<GptImage1EditImageImageSize>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<GptImage1EditImageNumImages>,
|
||||
|
||||
/// Quality of the image to generate. Default is High.
|
||||
pub image_quality: Option<GptImage1EditImageImageQuality>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GptImage1EditImageImageSize {
|
||||
Square, // 1:1, Default
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GptImage1EditImageNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GptImage1EditImageImageQuality {
|
||||
Auto,
|
||||
Low,
|
||||
Medium,
|
||||
High, // Default
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GptImage1EditImageResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod gpt_image_1_edit_image;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod edit;
|
||||
pub mod generate_flux_1_dev_text_to_image;
|
||||
pub mod generate_flux_1_schnell_text_to_image;
|
||||
pub mod generate_flux_pro_11_text_to_image;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
|
||||
@@ -18,10 +19,10 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
|
||||
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
|
||||
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
|
||||
@@ -158,6 +159,7 @@ use crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_
|
||||
use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::*;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::*;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::*;
|
||||
use crate::http_server::endpoints::generate::image::edit::gpt_image_1_edit_image_handler::*;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::*;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::*;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::*;
|
||||
@@ -234,6 +236,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler,
|
||||
crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler,
|
||||
crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler,
|
||||
crate::http_server::endpoints::generate::image::edit::gpt_image_1_edit_image_handler::gpt_image_1_edit_image_handler,
|
||||
crate::http_server::endpoints::generate::object::generate_hunyuan_2_1_image_to_3d_handler::generate_hunyuan_2_1_image_to_3d_handler,
|
||||
crate::http_server::endpoints::generate::object::generate_hunyuan_2_0_image_to_3d_handler::generate_hunyuan_2_0_image_to_3d_handler,
|
||||
crate::http_server::endpoints::generate::video::generate_kling_1_6_pro_video_handler::generate_kling_1_6_pro_video_handler,
|
||||
@@ -473,6 +476,11 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
EditUserFeatureFlagsError,
|
||||
EditUserFeatureFlagsOption,
|
||||
EditUserFeatureFlagsRequest,
|
||||
GptImage1EditImageRequest,
|
||||
GptImage1EditImageImageSize,
|
||||
GptImage1EditImageNumImages,
|
||||
GptImage1EditImageImageQuality,
|
||||
GptImage1EditImageResponse,
|
||||
EditUsernameError,
|
||||
EditUsernameRequest,
|
||||
EditUsernameResponse,
|
||||
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest, GptImage1EditImageResponse};
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::enqueue_gpt_image_1_edit_image_webhook;
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageNumImages;
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::{GptEditImageByokArgs, GptEditImageQuality, GptEditImageSize};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use utoipa::ToSchema;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
|
||||
/// Gpt Image 1 Image Editing
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Images (Edit)",
|
||||
path = "/v1/generate/image/edit/gpt_image_1",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GptImage1EditImageResponse),
|
||||
),
|
||||
params(
|
||||
("request" = GptImage1EditImageRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn gpt_image_1_edit_image_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GptImage1EditImageRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GptImage1EditImageResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(CommonWebError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
const CAN_SEE_DELETED: bool = false;
|
||||
|
||||
let tokens = match request.image_media_tokens.as_ref() {
|
||||
Some(tokens) => tokens,
|
||||
None => {
|
||||
warn!("No image media tokens provided");
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("No image media tokens provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let result = batch_get_media_files_by_tokens(
|
||||
&server_state.mysql_pool,
|
||||
tokens,
|
||||
CAN_SEE_DELETED,
|
||||
).await;
|
||||
|
||||
let media_files = match result {
|
||||
Ok(files) => files,
|
||||
Err(err) => {
|
||||
error!("Error getting media files by tokens: {:?}", err);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
if media_files.len() != tokens.len() {
|
||||
warn!("Wrong number of media files returned for tokens");
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Not all media files could be found.".to_string()));
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
let image_urls = media_files.iter()
|
||||
.map(|file| {
|
||||
let public_bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&file.public_bucket_directory_hash,
|
||||
file.maybe_public_bucket_prefix.as_deref(),
|
||||
file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&public_bucket_path);
|
||||
|
||||
media_links.cdn_url.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let image_size = match request.image_size {
|
||||
Some(GptImage1EditImageImageSize::Square) => GptEditImageSize::Square,
|
||||
Some(GptImage1EditImageImageSize::Horizontal) => GptEditImageSize::Horizontal,
|
||||
Some(GptImage1EditImageImageSize::Vertical) => GptEditImageSize::Vertical,
|
||||
None => GptEditImageSize::Square, // Default to Square
|
||||
};
|
||||
|
||||
let num_images = match request.num_images {
|
||||
Some(GptImage1EditImageNumImages::One) => GptEditImageNumImages::One,
|
||||
Some(GptImage1EditImageNumImages::Two) => GptEditImageNumImages::Two,
|
||||
Some(GptImage1EditImageNumImages::Three) => GptEditImageNumImages::Three,
|
||||
Some(GptImage1EditImageNumImages::Four) => GptEditImageNumImages::Four,
|
||||
None => GptEditImageNumImages::One, // Default to One
|
||||
};
|
||||
|
||||
let quality = match request.image_quality {
|
||||
Some(quality) => match quality {
|
||||
GptImage1EditImageImageQuality::Auto => GptEditImageQuality::Auto,
|
||||
GptImage1EditImageImageQuality::Low => GptEditImageQuality::Low,
|
||||
GptImage1EditImageImageQuality::Medium => GptEditImageQuality::Medium,
|
||||
GptImage1EditImageImageQuality::High => GptEditImageQuality::High,
|
||||
},
|
||||
None => GptEditImageQuality::High, // Default to High
|
||||
};
|
||||
|
||||
let openai_api_key = OpenAiApiKey::from_str(&server_state.openai.api_key);
|
||||
|
||||
let args = GptEditImageByokArgs {
|
||||
image_urls,
|
||||
prompt: request.prompt.as_deref().unwrap_or(""),
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
api_key: &server_state.fal.api_key,
|
||||
num_images,
|
||||
image_size,
|
||||
quality,
|
||||
openai_api_key: &openai_api_key,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_gpt_image_1_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_gpt_image_1_edit_image_webhook: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(GptImage1EditImageResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod gpt_image_1_edit_image_handler;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod edit;
|
||||
pub mod generate_flux_1_dev_text_to_image_handler;
|
||||
pub mod generate_flux_1_schnell_text_to_image_handler;
|
||||
pub mod generate_flux_pro_11_text_to_image_handler;
|
||||
|
||||
@@ -14,6 +14,7 @@ use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
use crate::http_server::endpoints::generate::image::edit::gpt_image_1_edit_image_handler::gpt_image_1_edit_image_handler;
|
||||
|
||||
pub fn add_generate_routes<T, B> (app: App<T>) -> App<T>
|
||||
where
|
||||
@@ -28,6 +29,12 @@ where
|
||||
{
|
||||
app.service(web::scope("/v1/generate")
|
||||
.service(web::scope("/image")
|
||||
.service(web::scope("/edit")
|
||||
.service(web::resource("/gpt_image_1")
|
||||
.route(web::post().to(gpt_image_1_edit_image_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::resource("/flux_1_dev_text_to_image")
|
||||
.route(web::post().to(generate_flux_1_dev_text_to_image_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
|
||||
@@ -80,7 +80,7 @@ use crate::http_server::web_utils::handle_multipart_error::handle_multipart_erro
|
||||
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
|
||||
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans};
|
||||
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans};
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread;
|
||||
use crate::threads::poll_ip_banlist_thread::poll_ip_bans;
|
||||
@@ -390,6 +390,8 @@ async fn main() -> AnyhowResult<()> {
|
||||
let fal_api_key = FalApiKey::new(easyenv::get_env_string_required("FAL_API_KEY")?);
|
||||
let fal_webhook_url = easyenv::get_env_string_required("FAL_WEBHOOK_URL")?;
|
||||
|
||||
let openai_api_key= easyenv::get_env_string_required("OPENAI_API_KEY")?;
|
||||
|
||||
let server_state = ServerState {
|
||||
env_config: EnvConfig {
|
||||
num_workers,
|
||||
@@ -433,6 +435,9 @@ async fn main() -> AnyhowResult<()> {
|
||||
api_key: fal_api_key,
|
||||
webhook_url: fal_webhook_url,
|
||||
},
|
||||
openai: OpenAiData {
|
||||
api_key: openai_api_key,
|
||||
},
|
||||
caches: InMemoryCaches {
|
||||
durable: DurableInMemoryCaches {
|
||||
model_token_info: model_token_info_cache,
|
||||
|
||||
@@ -88,6 +88,8 @@ pub struct ServerState {
|
||||
|
||||
pub fal: FalData,
|
||||
|
||||
pub openai: OpenAiData,
|
||||
|
||||
/// Where to store audio uploads for w2l
|
||||
pub audio_uploads_bucket_root: String,
|
||||
|
||||
@@ -293,3 +295,9 @@ pub struct FalData {
|
||||
pub api_key: FalApiKey,
|
||||
pub webhook_url: String,
|
||||
}
|
||||
|
||||
/// OpenAI integration
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAiData {
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -141,6 +141,7 @@ endpoints_fal-ai_flux-subject = []
|
||||
endpoints_fal-ai_fooocus = []
|
||||
endpoints_fal-ai_gemini-flash-edit = []
|
||||
endpoints_fal-ai_got-ocr = []
|
||||
endpoints_fal-ai_gpt = []
|
||||
endpoints_fal-ai_hunyuan-video = []
|
||||
endpoints_fal-ai_hunyuan-video-image-to-video = []
|
||||
endpoints_fal-ai_hunyuan-video-img2vid-lora = []
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::prelude::{Deserialize, FalRequest, Serialize};
|
||||
|
||||
//gpt-image-1/edit-image/byok
|
||||
|
||||
/*
|
||||
{
|
||||
"image_urls": [
|
||||
"https://storage.googleapis.com/falserverless/model_tests/gpt-image-1/cyberpunk.png",
|
||||
{
|
||||
"path": "assets_client_upload_media_4b47499df0289be2bf9a3ce9a4bf469a422642ad_media_01jwegn8zsew0a68em7m87fbf4.png"
|
||||
}
|
||||
],
|
||||
"prompt": "Make this pixel-art style.",
|
||||
"image_size": "1024x1024",
|
||||
"num_images": 1,
|
||||
"quality": "high",
|
||||
"openai_api_key": "key"
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptEditImageRequest {
|
||||
/// The context images
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// The prompt
|
||||
pub prompt: String,
|
||||
|
||||
pub image_size: String,
|
||||
|
||||
pub num_images: u8,
|
||||
|
||||
pub quality: String,
|
||||
|
||||
pub openai_api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ImageOutput {
|
||||
}
|
||||
|
||||
pub fn gpt_edit_image(params: GptEditImageRequest) -> FalRequest<GptEditImageRequest, ImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod byok;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod edit_image;
|
||||
@@ -908,6 +908,20 @@ pub mod gemini_flash_edit;
|
||||
)))
|
||||
)]
|
||||
pub mod got_ocr;
|
||||
#[cfg(any(
|
||||
feature = "endpoints",
|
||||
feature = "endpoints_fal-ai",
|
||||
feature = "endpoints_fal-ai_gpt"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "endpoints",
|
||||
feature = "endpoints_fal-ai",
|
||||
feature = "endpoints_fal-ai_gpt"
|
||||
)))
|
||||
)]
|
||||
pub mod gpt_image_1;
|
||||
#[cfg(any(
|
||||
feature = "endpoints",
|
||||
feature = "endpoints_fal-ai",
|
||||
|
||||
Vendored
+10
-4
@@ -144,13 +144,19 @@ impl<Params: Serialize, Response: DeserializeOwned> FalRequest<Params, Response>
|
||||
|
||||
info!("Sending request to FAL queue webhook: {}", request_url);
|
||||
|
||||
let response = self
|
||||
let builder = self
|
||||
.client
|
||||
.post(request_url)
|
||||
.post(request_url);
|
||||
|
||||
let builder = builder
|
||||
.json(&self.params)
|
||||
.header("Authorization", format!("Key {}", &key))
|
||||
.header("Content-Type", "application/json")
|
||||
.send()
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let response = builder
|
||||
.send();
|
||||
|
||||
let response = response
|
||||
.await?;
|
||||
|
||||
if response.status() != 200 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export enum API_TARGETS {
|
||||
// NB: Comment this out for quick testing in development
|
||||
//BASE_API = "http://localhost:12345",
|
||||
BASE_API = "https://api.storyteller.ai",
|
||||
BASE_API = "http://localhost:12345",
|
||||
//BASE_API = "https://api.storyteller.ai",
|
||||
GOOGLE_API = "https://storage.googleapis.com",
|
||||
FUNNEL_API = "https://style.storyteller.ai",
|
||||
CDN_API = "https://cdn.storyteller.ai",
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
CheckSoraSession,
|
||||
SoraSessionState,
|
||||
waitForSoraLogin,
|
||||
EnqueueContextualEditImage,
|
||||
EnqueueContextualEditImageModel,
|
||||
} from "@storyteller/tauri-api";
|
||||
|
||||
import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
|
||||
@@ -264,13 +266,14 @@ export const PromptBox2D = ({
|
||||
};
|
||||
|
||||
const handleTauriEnqueue = async () => {
|
||||
// Check if the Sora session is valid
|
||||
const soraSession = await CheckSoraSession();
|
||||
if (soraSession.state !== SoraSessionState.Valid) {
|
||||
setIsEnqueueing(false);
|
||||
await handleSoraLoginReminder();
|
||||
return;
|
||||
}
|
||||
// NB(bt): This needs to move to an error handler.
|
||||
// // Check if the Sora session is valid
|
||||
// const soraSession = await CheckSoraSession();
|
||||
// if (soraSession.state !== SoraSessionState.Valid) {
|
||||
// setIsEnqueueing(false);
|
||||
// await handleSoraLoginReminder();
|
||||
// return;
|
||||
// }
|
||||
|
||||
const api = new PromptsApi();
|
||||
let image = getCanvasRenderBitmap();
|
||||
@@ -318,15 +321,22 @@ export const PromptBox2D = ({
|
||||
//console.log("Generate response:", generateResponse);
|
||||
//toast.success("Please wait while we process your image.");
|
||||
|
||||
const aspectRatio = getCurrentSoraRemixAspectRatio();
|
||||
// TODO: Add context tokens
|
||||
// maybe_additional_images: referenceImages.map((image) => image.mediaToken),
|
||||
const imageMediaTokens = [
|
||||
snapshotMediaToken.data
|
||||
];
|
||||
|
||||
const generateResponse = await SoraImageRemix({
|
||||
snapshot_media_token: snapshotMediaToken.data,
|
||||
// TODO: Aspect ratio
|
||||
// const aspectRatio = getCurrentSoraRemixAspectRatio();
|
||||
|
||||
const generateResponse = await EnqueueContextualEditImage({
|
||||
model: EnqueueContextualEditImageModel.GptImage1,
|
||||
image_media_tokens: imageMediaTokens,
|
||||
disable_system_prompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
maybe_additional_images: referenceImages.map((image) => image.mediaToken),
|
||||
maybe_number_of_samples: 1,
|
||||
aspect_ratio: aspectRatio,
|
||||
image_count: 1,
|
||||
//aspect_ratio: aspectRatio,
|
||||
});
|
||||
|
||||
console.log("generateResponse", generateResponse);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./lib/common/CommandStatus.js";
|
||||
export * from "./lib/enqueue/EnqueueContextualEditImage.js";
|
||||
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
|
||||
export * from "./lib/enqueue/EnqueueTextToImage.js";
|
||||
export * from "./lib/enqueue/EnqueueImageToVideo.js";
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
|
||||
export interface EnqueueContextualEditImageRequest {
|
||||
|
||||
/// The model to use.
|
||||
model?: EnqueueContextualEditImageModel;
|
||||
|
||||
/// Images to use for the image edit.
|
||||
/// The first image is typically a 2D canvas or 3D stage, but doesn't have to be.
|
||||
/// There must be at least one image.
|
||||
image_media_tokens?: string[];
|
||||
|
||||
/// The user's image generation prompt.
|
||||
prompt?: string;
|
||||
|
||||
/// Turn off the system prompt.
|
||||
disable_system_prompt?: boolean;
|
||||
|
||||
/// Number of images to generate.
|
||||
image_count?: number;
|
||||
|
||||
aspect_ratio?: EnqueueContextualEditImageSize;
|
||||
|
||||
image_quality?: EnqueueContextualEditImageQuality;
|
||||
}
|
||||
|
||||
export enum EnqueueContextualEditImageErrorType {
|
||||
ModelNotSpecified = "model_not_specified",
|
||||
NoProviderAvailable = "no_provider_available",
|
||||
BadRequest = "bad_request",
|
||||
ServerError = "server_error",
|
||||
TooManyConcurrentTasks = "too_many_concurrent_tasks",
|
||||
SoraLoginRequired = "sora_login_required",
|
||||
SoraUsernameNotYetCreated = "sora_username_not_yet_created",
|
||||
SoraIsHavingProblems = "sora_is_having_problems",
|
||||
}
|
||||
|
||||
export enum EnqueueContextualEditImageModel {
|
||||
GptImage1 = "gpt_image_1",
|
||||
}
|
||||
|
||||
export enum EnqueueContextualEditImageSize {
|
||||
Auto = "auto",
|
||||
Square = "square",
|
||||
Wide = "wide",
|
||||
Tall = "tall",
|
||||
}
|
||||
|
||||
export enum EnqueueContextualEditImageQuality {
|
||||
Auto = "auto",
|
||||
High = "high",
|
||||
Medium = "medium",
|
||||
Low = "low",
|
||||
}
|
||||
|
||||
export interface EnqueueContextualEditImageError extends CommandResult {
|
||||
error_type: EnqueueContextualEditImageErrorType;
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
export interface EnqueueContextualEditImagePayload {
|
||||
}
|
||||
|
||||
export interface EnqueueContextualEditImageSuccess extends CommandResult {
|
||||
payload: EnqueueContextualEditImagePayload;
|
||||
}
|
||||
|
||||
export type EnqueueContextualEditImageResult = EnqueueContextualEditImageSuccess | EnqueueContextualEditImageError;
|
||||
|
||||
export const EnqueueContextualEditImage = async (request: EnqueueContextualEditImageRequest) : Promise<EnqueueContextualEditImageResult> => {
|
||||
const result = await invoke("enqueue_contextual_edit_image_command", {
|
||||
request: request,
|
||||
});
|
||||
return (result as EnqueueContextualEditImageResult);
|
||||
}
|
||||
Reference in New Issue
Block a user