mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
include prompts with fal, fix alerts (#1507)
This commit is contained in:
+10
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n queue_status_url,\n queue_response_url,\n frontend_subscriber_id,\n frontend_subscriber_payload\n FROM tasks\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n queue_status_url,\n queue_response_url,\n prompt_token,\n frontend_subscriber_id,\n frontend_subscriber_payload\n FROM tasks\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -49,14 +49,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_subscriber_id",
|
||||
"name": "prompt_token",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_subscriber_payload",
|
||||
"name": "frontend_subscriber_id",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_subscriber_payload",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -73,8 +78,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "68f9fed45fc55aa2ec22dc1ed22795d0c99882917abb9b52b78735b8194c2624"
|
||||
"hash": "35677894f7b02f32a1bf6396fa6d10caba69bb0f3f03ee7ffbfa1437689d42df"
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n queue_status_url,\n queue_response_url,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n queue_status_url,\n queue_response_url,\n prompt_token,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 11
|
||||
"Right": 12
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "15a2f8712c80b72c86d9e77eb590d74ea21dd818a424267bcf5b88f9c993c300"
|
||||
"hash": "4fc9674ae39abaadc36054c200290ec95d0e2c7765336f0fa97805a2265f5c4c"
|
||||
}
|
||||
@@ -19,7 +19,8 @@
|
||||
-- tasks_v6.sqlite - added two fields:
|
||||
-- on_failure_type
|
||||
-- on_failure_message
|
||||
-- tasks_v7.sqlite - added two fields (for fal):
|
||||
-- tasks_v7.sqlite - added prompt token + two fields (for fal):
|
||||
-- prompt_token
|
||||
-- queue_status_url
|
||||
-- queue_response_url
|
||||
|
||||
@@ -49,6 +50,11 @@ CREATE TABLE tasks (
|
||||
-- The primary key for the job in the provider's system.
|
||||
provider_job_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- For third party jobs, we need to record the prompt details upfront.
|
||||
-- We create a prompt record and store the token to associate once the task completes.
|
||||
prompt_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- Tell the job system which caller initiated the task.
|
||||
-- e.g. 'canvas'
|
||||
|
||||
+7
-4
@@ -34,10 +34,9 @@ pub struct LegacyUploadMediaFileFromFileArgs<'a, P: AsRef<Path>> {
|
||||
// /// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
|
||||
// pub maybe_prompt_token: Option<&'a PromptToken>,
|
||||
|
||||
// /// If provided, this is the service provider that created the image.
|
||||
// /// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
|
||||
// pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
|
||||
/// If provided, the third-party provider that generated this file.
|
||||
pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
|
||||
// /// If provided, this groups the file into a batch
|
||||
// /// TODO: This shouldn't be set clientside without the backend generating the token
|
||||
// /// and cryptographically securing it. But we need to go fast here.
|
||||
@@ -82,6 +81,10 @@ pub async fn legacy_upload_media_file_from_file<P: AsRef<Path>>(
|
||||
// form = form.text("maybe_batch_token", batch_token.to_string());
|
||||
//}
|
||||
|
||||
if let Some(provider) = &args.maybe_generation_provider {
|
||||
form = form.text("maybe_generation_provider", provider.to_str().to_string());
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
+8
@@ -6,6 +6,7 @@ use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
@@ -31,6 +32,9 @@ pub struct UploadImageBytesArgs<'a> {
|
||||
|
||||
/// If true, we should hide the image from the user's gallery.
|
||||
pub is_intermediate_system_file: bool,
|
||||
|
||||
/// If provided, the third-party provider that generated this file.
|
||||
pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -86,6 +90,10 @@ pub async fn upload_image_media_file_from_bytes(
|
||||
form = form.text("is_intermediate_system_file", "true");
|
||||
}
|
||||
|
||||
if let Some(provider) = &args.maybe_generation_provider {
|
||||
form = form.text("maybe_generation_provider", provider.to_str().to_string());
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
+7
-4
@@ -34,10 +34,9 @@ pub struct UploadImageFromFileArgs<'a, P: AsRef<Path>> {
|
||||
/// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
|
||||
pub maybe_prompt_token: Option<&'a PromptToken>,
|
||||
|
||||
// /// If provided, this is the service provider that created the image.
|
||||
// /// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
|
||||
// pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
|
||||
/// If provided, the third-party provider that generated this file.
|
||||
pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
|
||||
/// If provided, this groups the file into a batch
|
||||
/// TODO: This shouldn't be set clientside without the backend generating the token
|
||||
/// and cryptographically securing it. But we need to go fast here.
|
||||
@@ -82,6 +81,10 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
|
||||
form = form.text("maybe_batch_token", batch_token.to_string());
|
||||
}
|
||||
|
||||
if let Some(provider) = &args.maybe_generation_provider {
|
||||
form = form.text("maybe_generation_provider", provider.to_str().to_string());
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
+7
-1
@@ -4,6 +4,7 @@ use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
@@ -24,6 +25,7 @@ pub async fn upload_new_engine_asset_from_file<P: AsRef<Path>>(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
path: P,
|
||||
maybe_generation_provider: Option<GenerationProvider>,
|
||||
) -> Result<UploadImageMediaFileSuccessResponse, ApiError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
@@ -37,11 +39,15 @@ pub async fn upload_new_engine_asset_from_file<P: AsRef<Path>>(
|
||||
let file_bytes = std::fs::read(path.as_ref())?;
|
||||
let file_name = path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let form = Form::new()
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.text("engine_category", "object") // TODO: Which type should we use?
|
||||
.part("file", Part::bytes(file_bytes).file_name(file_name));
|
||||
|
||||
if let Some(provider) = &maybe_generation_provider {
|
||||
form = form.text("maybe_generation_provider", provider.to_str().to_string());
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
+7
-3
@@ -4,6 +4,7 @@ use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
@@ -31,9 +32,8 @@ pub struct UploadVideoFromFileArgs<'a, P: AsRef<Path>> {
|
||||
/// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
|
||||
pub maybe_prompt_token: Option<&'a PromptToken>,
|
||||
|
||||
// /// If provided, this is the service provider that created the image.
|
||||
// /// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
|
||||
// pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
/// If provided, the third-party provider that generated this file.
|
||||
pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
|
||||
// /// If provided, this groups the file into a batch
|
||||
// /// TODO: This shouldn't be set clientside without the backend generating the token
|
||||
@@ -73,6 +73,10 @@ pub async fn upload_video_media_file_from_file<P: AsRef<Path>>(
|
||||
form = form.text("maybe_prompt_token", prompt_token.to_string());
|
||||
}
|
||||
|
||||
if let Some(provider) = &args.maybe_generation_provider {
|
||||
form = form.text("maybe_generation_provider", provider.to_str().to_string());
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
+4
-2
@@ -33,7 +33,7 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
|
||||
|
||||
match maybe_type {
|
||||
Some(FileExtension::Glb) => {
|
||||
match upload_new_engine_asset_from_file(api_host, maybe_creds, path).await {
|
||||
match upload_new_engine_asset_from_file(api_host, maybe_creds, path, None).await {
|
||||
Ok(result) => Ok(UploadMediaFileSuccessResponse {
|
||||
success: result.success,
|
||||
media_file_token: result.media_file_token,
|
||||
@@ -46,7 +46,8 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
|
||||
api_host,
|
||||
maybe_creds,
|
||||
path,
|
||||
maybe_prompt_token
|
||||
maybe_prompt_token,
|
||||
maybe_generation_provider: None,
|
||||
}).await {
|
||||
Ok(result) => Ok(UploadMediaFileSuccessResponse {
|
||||
success: result.success,
|
||||
@@ -63,6 +64,7 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
|
||||
is_intermediate_system_file: false,
|
||||
maybe_prompt_token,
|
||||
maybe_batch_token: None,
|
||||
maybe_generation_provider: None,
|
||||
}).await;
|
||||
match result {
|
||||
Ok(result) => Ok(UploadMediaFileSuccessResponse {
|
||||
|
||||
+1
@@ -125,6 +125,7 @@ pub async fn handle_artcraft_flux_kontext_edit(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -123,6 +123,7 @@ pub async fn handle_artcraft_gpt_image_1_edit(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -110,6 +110,7 @@ pub(super) async fn handle_image_edit_artcraft_via_router(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -177,6 +177,7 @@ pub async fn handle_sora_gpt_image_1_edit(
|
||||
provider: GenerationProvider::Sora,
|
||||
provider_job_id: Some(response.task_id.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+2
@@ -114,6 +114,7 @@ pub async fn handle_artcraft_flux_dev_juggernaut_inpaint(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageInpaintEdit,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
@@ -149,6 +150,7 @@ async fn get_mask(
|
||||
image_bytes: image_bytes.0,
|
||||
image_type: ImageType::Png,
|
||||
is_intermediate_system_file: true,
|
||||
maybe_generation_provider: None,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image media file: {:?}", err);
|
||||
|
||||
+2
@@ -113,6 +113,7 @@ pub async fn handle_artcraft_flux_pro_1_inpaint(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageInpaintEdit,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
@@ -148,6 +149,7 @@ async fn get_mask(
|
||||
image_bytes: image_bytes.0,
|
||||
image_type: ImageType::Png,
|
||||
is_intermediate_system_file: true,
|
||||
maybe_generation_provider: None,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image media file: {:?}", err);
|
||||
|
||||
+1
@@ -126,6 +126,7 @@ pub async fn handle_artcraft_flux_pro_kontext_inpaint(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageInpaintEdit,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -111,6 +111,7 @@ pub async fn handle_artcraft_nano_banana_inpaint(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageInpaintEdit,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -109,6 +109,7 @@ pub async fn handle_artcraft_gpt_image_1_text_to_image(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -88,6 +88,7 @@ pub(super) async fn handle_text_to_image_artcraft_via_router(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ pub async fn handle_grok(
|
||||
provider_job_id: Some(job_id),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -112,6 +112,7 @@ pub async fn handle_midjourney(
|
||||
provider_job_id: Some(job_id),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -112,6 +112,7 @@ pub async fn handle_gpt_image_1_sora_text_to_image(
|
||||
model: Some(GenerationModel::GptImage1),
|
||||
provider_job_id: Some(response.task_id.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+2
@@ -80,6 +80,7 @@ pub async fn handle_generic_bg_removal_artcraft(
|
||||
provider_job_id: Some(success_result.inference_job_token.to_string()),
|
||||
model: None,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
@@ -114,6 +115,7 @@ async fn upload_image_from_base64_bytes(
|
||||
is_intermediate_system_file: true, // NB: Probably not essential to keep this.
|
||||
maybe_prompt_token: None, // NB: Not used for bg removal.
|
||||
maybe_batch_token: None, // NB: Not used for bg removal.
|
||||
maybe_generation_provider: None,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image media file: {:?}", err);
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ pub(super) async fn handle_artcraft_splat_via_router(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -103,6 +103,7 @@ pub async fn handle_worldlabs_marble(
|
||||
provider_job_id: Some(world_id.0),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -153,6 +153,7 @@ pub async fn handle_artcraft_object(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use enums::tauri::tasks::task_type::TaskType;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use sqlite_tasks::error::SqliteTasksError;
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
pub struct TaskEnqueueSuccess {
|
||||
@@ -16,6 +17,7 @@ pub struct TaskEnqueueSuccess {
|
||||
pub provider_job_id: Option<String>,
|
||||
pub maybe_queue_status_url: Option<String>,
|
||||
pub maybe_queue_response_url: Option<String>,
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
}
|
||||
|
||||
impl TaskEnqueueSuccess{
|
||||
@@ -124,6 +126,7 @@ impl TaskEnqueueSuccess{
|
||||
provider_job_id: self.provider_job_id.as_deref(),
|
||||
queue_status_url: self.maybe_queue_status_url.as_deref(),
|
||||
queue_response_url: self.maybe_queue_response_url.as_deref(),
|
||||
prompt_token: self.maybe_prompt_token.as_ref(),
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod router_image_request_to_artcraft_prompt;
|
||||
pub mod router_video_request_to_artcraft_prompt;
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
use artcraft_api_defs::prompts::create_prompt::CreatePromptRequest;
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterAspectRatio;
|
||||
use artcraft_router::api::common_image_model::CommonImageModel;
|
||||
use artcraft_router::api::common_resolution::CommonResolution as RouterResolution;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as EnumsAspectRatio;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation::common_resolution::CommonResolution as EnumsResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
|
||||
pub fn router_image_request_to_artcraft_prompt(
|
||||
request: &GenerateImageRequestBuilder,
|
||||
) -> CreatePromptRequest {
|
||||
CreatePromptRequest {
|
||||
uuid_idempotency_token: generate_random_uuid(),
|
||||
positive_prompt: request.prompt.clone(),
|
||||
negative_prompt: None,
|
||||
model_type: image_model_to_common_model_type(request.model),
|
||||
generation_provider: Some(provider_to_generation_provider(request.provider)),
|
||||
maybe_generation_mode: Some(determine_image_generation_mode(request)),
|
||||
maybe_aspect_ratio: request.aspect_ratio.map(router_aspect_ratio_to_enums),
|
||||
maybe_resolution: request.resolution.map(router_resolution_to_enums),
|
||||
maybe_batch_count: request.image_batch_count.map(|n| n.min(255) as u8),
|
||||
maybe_generate_audio: None,
|
||||
maybe_duration_seconds: None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Converters ──
|
||||
|
||||
fn determine_image_generation_mode(request: &GenerateImageRequestBuilder) -> CommonGenerationMode {
|
||||
if request.image_inputs.is_some() {
|
||||
CommonGenerationMode::Edit
|
||||
} else {
|
||||
CommonGenerationMode::Text
|
||||
}
|
||||
}
|
||||
|
||||
fn image_model_to_common_model_type(model: CommonImageModel) -> Option<CommonModelType> {
|
||||
match model {
|
||||
CommonImageModel::Flux1Dev => Some(CommonModelType::Flux1Dev),
|
||||
CommonImageModel::Flux1Schnell => Some(CommonModelType::Flux1Schnell),
|
||||
CommonImageModel::FluxPro11 => Some(CommonModelType::FluxPro11),
|
||||
CommonImageModel::FluxPro11Ultra => Some(CommonModelType::FluxPro11Ultra),
|
||||
CommonImageModel::GptImage1 => Some(CommonModelType::GptImage1),
|
||||
CommonImageModel::GptImage1p5 => Some(CommonModelType::GptImage1p5),
|
||||
CommonImageModel::GptImage2 => Some(CommonModelType::GptImage2),
|
||||
CommonImageModel::NanoBanana => Some(CommonModelType::NanoBanana),
|
||||
CommonImageModel::NanoBanana2 => Some(CommonModelType::NanoBanana2),
|
||||
CommonImageModel::NanoBananaPro => Some(CommonModelType::NanoBananaPro),
|
||||
CommonImageModel::Seedream4 => Some(CommonModelType::Seedream4),
|
||||
CommonImageModel::Seedream4p5 => Some(CommonModelType::Seedream4p5),
|
||||
CommonImageModel::Seedream5Lite => Some(CommonModelType::Seedream5Lite),
|
||||
CommonImageModel::QwenEdit2511Angles => Some(CommonModelType::QwenEdit2511Angles),
|
||||
CommonImageModel::Flux2LoraAngles => Some(CommonModelType::Flux2LoraAngles),
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_to_generation_provider(provider: Provider) -> GenerationProvider {
|
||||
match provider {
|
||||
Provider::Artcraft => GenerationProvider::Artcraft,
|
||||
Provider::Fal => GenerationProvider::Fal,
|
||||
// Unused providers -> ArtCraft
|
||||
Provider::Muapi => GenerationProvider::Artcraft,
|
||||
Provider::Seedance2Pro => GenerationProvider::Artcraft ,
|
||||
}
|
||||
}
|
||||
|
||||
fn router_aspect_ratio_to_enums(ar: RouterAspectRatio) -> EnumsAspectRatio {
|
||||
match ar {
|
||||
RouterAspectRatio::Auto => EnumsAspectRatio::Auto,
|
||||
RouterAspectRatio::Square => EnumsAspectRatio::Square,
|
||||
RouterAspectRatio::WideThreeByTwo => EnumsAspectRatio::WideThreeByTwo,
|
||||
RouterAspectRatio::WideFourByThree => EnumsAspectRatio::WideFourByThree,
|
||||
RouterAspectRatio::WideFiveByFour => EnumsAspectRatio::WideFiveByFour,
|
||||
RouterAspectRatio::WideSixteenByNine => EnumsAspectRatio::WideSixteenByNine,
|
||||
RouterAspectRatio::WideTwentyOneByNine => EnumsAspectRatio::WideTwentyOneByNine,
|
||||
RouterAspectRatio::TallTwoByThree => EnumsAspectRatio::TallTwoByThree,
|
||||
RouterAspectRatio::TallThreeByFour => EnumsAspectRatio::TallThreeByFour,
|
||||
RouterAspectRatio::TallFourByFive => EnumsAspectRatio::TallFourByFive,
|
||||
RouterAspectRatio::TallNineBySixteen => EnumsAspectRatio::TallNineBySixteen,
|
||||
RouterAspectRatio::TallNineByTwentyOne => EnumsAspectRatio::TallNineByTwentyOne,
|
||||
RouterAspectRatio::Wide => EnumsAspectRatio::Wide,
|
||||
RouterAspectRatio::Tall => EnumsAspectRatio::Tall,
|
||||
RouterAspectRatio::Auto2k => EnumsAspectRatio::Auto2k,
|
||||
RouterAspectRatio::Auto3k => EnumsAspectRatio::Auto3k,
|
||||
RouterAspectRatio::Auto4k => EnumsAspectRatio::Auto4k,
|
||||
RouterAspectRatio::SquareHd => EnumsAspectRatio::SquareHd,
|
||||
}
|
||||
}
|
||||
|
||||
fn router_resolution_to_enums(res: RouterResolution) -> EnumsResolution {
|
||||
match res {
|
||||
RouterResolution::OneK => EnumsResolution::OneK,
|
||||
RouterResolution::TwoK => EnumsResolution::TwoK,
|
||||
RouterResolution::ThreeK => EnumsResolution::ThreeK,
|
||||
RouterResolution::FourK => EnumsResolution::FourK,
|
||||
RouterResolution::HalfK => EnumsResolution::HalfK,
|
||||
RouterResolution::FourEightyP => EnumsResolution::FourEightyP,
|
||||
RouterResolution::SevenTwentyP => EnumsResolution::SevenTwentyP,
|
||||
RouterResolution::TenEightyP => EnumsResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_conversion() {
|
||||
let builder = base_builder();
|
||||
let prompt = router_image_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.positive_prompt.as_deref(), Some("a cat in space"));
|
||||
assert_eq!(prompt.model_type, Some(CommonModelType::NanoBananaPro));
|
||||
assert_eq!(prompt.generation_provider, Some(GenerationProvider::Fal));
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Text));
|
||||
assert!(prompt.negative_prompt.is_none());
|
||||
assert!(prompt.maybe_aspect_ratio.is_none());
|
||||
assert!(prompt.maybe_resolution.is_none());
|
||||
assert!(prompt.maybe_batch_count.is_none());
|
||||
assert!(!prompt.uuid_idempotency_token.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_mode_when_no_images() {
|
||||
let builder = base_builder();
|
||||
let prompt = router_image_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Text));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_mode_when_images_present() {
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_image_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Edit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_aspect_ratio_and_resolution() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(RouterResolution::TwoK),
|
||||
image_batch_count: Some(3),
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_image_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_aspect_ratio, Some(EnumsAspectRatio::WideSixteenByNine));
|
||||
assert_eq!(prompt.maybe_resolution, Some(EnumsResolution::TwoK));
|
||||
assert_eq!(prompt.maybe_batch_count, Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_prompt() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
prompt: None,
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_image_request_to_artcraft_prompt(&builder);
|
||||
assert!(prompt.positive_prompt.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_image_models_map() {
|
||||
let models = [
|
||||
(CommonImageModel::Flux1Dev, CommonModelType::Flux1Dev),
|
||||
(CommonImageModel::Flux1Schnell, CommonModelType::Flux1Schnell),
|
||||
(CommonImageModel::NanoBananaPro, CommonModelType::NanoBananaPro),
|
||||
(CommonImageModel::GptImage1, CommonModelType::GptImage1),
|
||||
(CommonImageModel::Seedream4, CommonModelType::Seedream4),
|
||||
];
|
||||
for (router_model, expected) in models {
|
||||
let builder = GenerateImageRequestBuilder { model: router_model, ..base_builder() };
|
||||
let prompt = router_image_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.model_type, Some(expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_mapping() {
|
||||
let builder = GenerateImageRequestBuilder { provider: Provider::Artcraft, ..base_builder() };
|
||||
let prompt = router_image_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.generation_provider, Some(GenerationProvider::Artcraft));
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
use artcraft_api_defs::prompts::create_prompt::CreatePromptRequest;
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterAspectRatio;
|
||||
use artcraft_router::api::common_resolution::CommonResolution as RouterResolution;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as EnumsAspectRatio;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation::common_resolution::CommonResolution as EnumsResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
|
||||
pub fn router_video_request_to_artcraft_prompt(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
) -> CreatePromptRequest {
|
||||
CreatePromptRequest {
|
||||
uuid_idempotency_token: generate_random_uuid(),
|
||||
positive_prompt: request.prompt.clone(),
|
||||
negative_prompt: request.negative_prompt.clone(),
|
||||
model_type: video_model_to_common_model_type(request.model),
|
||||
generation_provider: Some(provider_to_generation_provider(request.provider)),
|
||||
maybe_generation_mode: Some(determine_video_generation_mode(request)),
|
||||
maybe_aspect_ratio: request.aspect_ratio.map(router_aspect_ratio_to_enums),
|
||||
maybe_resolution: request.resolution.map(router_resolution_to_enums),
|
||||
maybe_batch_count: request.video_batch_count.map(|n| n.min(255) as u8),
|
||||
maybe_generate_audio: request.generate_audio,
|
||||
maybe_duration_seconds: request.duration_seconds.map(|d| d as u32),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Converters ──
|
||||
|
||||
fn determine_video_generation_mode(request: &GenerateVideoRequestBuilder) -> CommonGenerationMode {
|
||||
let has_keyframes = request.start_frame.is_some() || request.end_frame.is_some();
|
||||
let has_references = request.reference_images.is_some()
|
||||
|| request.reference_videos.is_some()
|
||||
|| request.reference_audio.is_some();
|
||||
|
||||
if has_keyframes {
|
||||
CommonGenerationMode::Keyframe
|
||||
} else if has_references {
|
||||
CommonGenerationMode::Reference
|
||||
} else {
|
||||
CommonGenerationMode::Text
|
||||
}
|
||||
}
|
||||
|
||||
fn video_model_to_common_model_type(model: CommonVideoModel) -> Option<CommonModelType> {
|
||||
match model {
|
||||
CommonVideoModel::GrokVideo => Some(CommonModelType::GrokVideo),
|
||||
CommonVideoModel::Kling16Pro => Some(CommonModelType::Kling16Pro),
|
||||
CommonVideoModel::Kling21Pro => Some(CommonModelType::Kling21Pro),
|
||||
CommonVideoModel::Kling21Master => Some(CommonModelType::Kling21Master),
|
||||
CommonVideoModel::Kling2p5TurboPro => Some(CommonModelType::Kling2p5TurboPro),
|
||||
CommonVideoModel::Kling2p6Pro => Some(CommonModelType::Kling2p6Pro),
|
||||
CommonVideoModel::Kling3p0Standard => Some(CommonModelType::Kling3p0Standard),
|
||||
CommonVideoModel::Kling3p0Pro => Some(CommonModelType::Kling3p0Pro),
|
||||
CommonVideoModel::Seedance10Lite => Some(CommonModelType::Seedance10Lite),
|
||||
CommonVideoModel::Seedance1p5Pro => Some(CommonModelType::Seedance1p5Pro),
|
||||
CommonVideoModel::Seedance2p0 => Some(CommonModelType::Seedance2p0),
|
||||
CommonVideoModel::Seedance2p0Fast => Some(CommonModelType::Seedance2p0Fast),
|
||||
CommonVideoModel::HappyHorse1p0 => Some(CommonModelType::HappyHorse1p0),
|
||||
CommonVideoModel::Sora2 => Some(CommonModelType::Sora2),
|
||||
CommonVideoModel::Sora2Pro => Some(CommonModelType::Sora2Pro),
|
||||
CommonVideoModel::Veo2 => Some(CommonModelType::Veo2),
|
||||
CommonVideoModel::Veo3 => Some(CommonModelType::Veo3),
|
||||
CommonVideoModel::Veo3Fast => Some(CommonModelType::Veo3Fast),
|
||||
CommonVideoModel::Veo3p1 => Some(CommonModelType::Veo3p1),
|
||||
CommonVideoModel::Veo3p1Fast => Some(CommonModelType::Veo3p1Fast),
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_to_generation_provider(provider: Provider) -> GenerationProvider {
|
||||
match provider {
|
||||
Provider::Artcraft => GenerationProvider::Artcraft,
|
||||
Provider::Fal => GenerationProvider::Fal,
|
||||
// Unused providers -> ArtCraft
|
||||
Provider::Muapi => GenerationProvider::Artcraft,
|
||||
Provider::Seedance2Pro => GenerationProvider::Artcraft ,
|
||||
}
|
||||
}
|
||||
|
||||
fn router_aspect_ratio_to_enums(ar: RouterAspectRatio) -> EnumsAspectRatio {
|
||||
match ar {
|
||||
RouterAspectRatio::Auto => EnumsAspectRatio::Auto,
|
||||
RouterAspectRatio::Square => EnumsAspectRatio::Square,
|
||||
RouterAspectRatio::WideThreeByTwo => EnumsAspectRatio::WideThreeByTwo,
|
||||
RouterAspectRatio::WideFourByThree => EnumsAspectRatio::WideFourByThree,
|
||||
RouterAspectRatio::WideFiveByFour => EnumsAspectRatio::WideFiveByFour,
|
||||
RouterAspectRatio::WideSixteenByNine => EnumsAspectRatio::WideSixteenByNine,
|
||||
RouterAspectRatio::WideTwentyOneByNine => EnumsAspectRatio::WideTwentyOneByNine,
|
||||
RouterAspectRatio::TallTwoByThree => EnumsAspectRatio::TallTwoByThree,
|
||||
RouterAspectRatio::TallThreeByFour => EnumsAspectRatio::TallThreeByFour,
|
||||
RouterAspectRatio::TallFourByFive => EnumsAspectRatio::TallFourByFive,
|
||||
RouterAspectRatio::TallNineBySixteen => EnumsAspectRatio::TallNineBySixteen,
|
||||
RouterAspectRatio::TallNineByTwentyOne => EnumsAspectRatio::TallNineByTwentyOne,
|
||||
RouterAspectRatio::Wide => EnumsAspectRatio::Wide,
|
||||
RouterAspectRatio::Tall => EnumsAspectRatio::Tall,
|
||||
RouterAspectRatio::Auto2k => EnumsAspectRatio::Auto2k,
|
||||
RouterAspectRatio::Auto3k => EnumsAspectRatio::Auto3k,
|
||||
RouterAspectRatio::Auto4k => EnumsAspectRatio::Auto4k,
|
||||
RouterAspectRatio::SquareHd => EnumsAspectRatio::SquareHd,
|
||||
}
|
||||
}
|
||||
|
||||
fn router_resolution_to_enums(res: RouterResolution) -> EnumsResolution {
|
||||
match res {
|
||||
RouterResolution::OneK => EnumsResolution::OneK,
|
||||
RouterResolution::TwoK => EnumsResolution::TwoK,
|
||||
RouterResolution::ThreeK => EnumsResolution::ThreeK,
|
||||
RouterResolution::FourK => EnumsResolution::FourK,
|
||||
RouterResolution::HalfK => EnumsResolution::HalfK,
|
||||
RouterResolution::FourEightyP => EnumsResolution::FourEightyP,
|
||||
RouterResolution::SevenTwentyP => EnumsResolution::SevenTwentyP,
|
||||
RouterResolution::TenEightyP => EnumsResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a dog running on the beach".to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy:
|
||||
artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_conversion() {
|
||||
let builder = base_builder();
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.positive_prompt.as_deref(), Some("a dog running on the beach"));
|
||||
assert_eq!(prompt.model_type, Some(CommonModelType::Kling3p0Standard));
|
||||
assert_eq!(prompt.generation_provider, Some(GenerationProvider::Fal));
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Text));
|
||||
assert!(prompt.negative_prompt.is_none());
|
||||
assert!(prompt.maybe_aspect_ratio.is_none());
|
||||
assert!(prompt.maybe_resolution.is_none());
|
||||
assert!(prompt.maybe_batch_count.is_none());
|
||||
assert!(prompt.maybe_generate_audio.is_none());
|
||||
assert!(prompt.maybe_duration_seconds.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_fields() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
negative_prompt: Some("blurry".to_string()),
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
duration_seconds: Some(10),
|
||||
video_batch_count: Some(2),
|
||||
generate_audio: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.negative_prompt.as_deref(), Some("blurry"));
|
||||
assert_eq!(prompt.maybe_aspect_ratio, Some(EnumsAspectRatio::WideSixteenByNine));
|
||||
assert_eq!(prompt.maybe_resolution, Some(EnumsResolution::TenEightyP));
|
||||
assert_eq!(prompt.maybe_duration_seconds, Some(10));
|
||||
assert_eq!(prompt.maybe_batch_count, Some(2));
|
||||
assert_eq!(prompt.maybe_generate_audio, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_mode_when_no_references() {
|
||||
let builder = base_builder();
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Text));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyframe_mode_with_start_frame() {
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/frame.jpg".to_string())),
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Keyframe));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyframe_mode_with_end_frame() {
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
end_frame: Some(ImageRef::Url("https://example.com/frame.jpg".to_string())),
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Keyframe));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_mode_with_reference_images() {
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/ref.jpg".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Reference));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyframe_takes_priority_over_reference() {
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/frame.jpg".to_string())),
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/ref.jpg".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.maybe_generation_mode, Some(CommonGenerationMode::Keyframe));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_model_mapping() {
|
||||
let models = [
|
||||
(CommonVideoModel::Kling3p0Standard, CommonModelType::Kling3p0Standard),
|
||||
(CommonVideoModel::Veo3, CommonModelType::Veo3),
|
||||
(CommonVideoModel::Seedance2p0, CommonModelType::Seedance2p0),
|
||||
(CommonVideoModel::Sora2, CommonModelType::Sora2),
|
||||
];
|
||||
for (router_model, expected) in models {
|
||||
let builder = GenerateVideoRequestBuilder { model: router_model, ..base_builder() };
|
||||
let prompt = router_video_request_to_artcraft_prompt(&builder);
|
||||
assert_eq!(prompt.model_type, Some(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_fr
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft;
|
||||
use crate::core::commands::generate::generate_image::providers::router::handle_router::handle_router;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft_router::handle_router::handle_router;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::{
|
||||
TauriGenerateImageErrorType, TauriGenerateImageRequest, TauriGenerateImageResponse,
|
||||
};
|
||||
@@ -54,6 +54,7 @@ pub async fn generate_image_command(
|
||||
other,
|
||||
&app_env_configs,
|
||||
&credential_cache,
|
||||
&storyteller_creds_manager,
|
||||
).await
|
||||
}
|
||||
};
|
||||
|
||||
+1
@@ -66,6 +66,7 @@ pub async fn handle_artcraft_via_omni_endpoint(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(response.inference_job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ pub async fn handle_flux_2_lora_angles(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -77,6 +77,7 @@ pub async fn handle_flux_dev_juggernaut_inpaint(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(response.inference_job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ pub async fn handle_flux_pro_kontext_edit(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(response.inference_job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ pub async fn handle_qwen_edit_2511_angles(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+51
-6
@@ -1,3 +1,4 @@
|
||||
use artcraft_client::endpoints::prompts::create_prompt::create_prompt;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
@@ -11,18 +12,21 @@ use artcraft_router::generate::generate_image::generate_image_response::Generate
|
||||
use artcraft_router::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use log::{info, warn};
|
||||
use log::{error, info, warn};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
|
||||
use crate::core::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model;
|
||||
use crate::core::api_adapters::models::image::tauri_image_model_to_router_model::tauri_image_model_to_router_model;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::router::utils::convert_enums_to_router::{convert_aspect_ratio, convert_quality, convert_resolution};
|
||||
use crate::core::commands::generate::generate_image::providers::router::utils::map_media_files_to_urls::map_media_file_tokens_to_cdn_urls;
|
||||
use crate::core::commands::generate::common::router_image_request_to_artcraft_prompt::router_image_request_to_artcraft_prompt;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft_router::utils::convert_enums_to_router::{convert_aspect_ratio, convert_quality, convert_resolution};
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft_router::utils::map_media_files_to_urls::map_media_file_tokens_to_cdn_urls;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
|
||||
/// Handle image generation for providers that authenticate via API key.
|
||||
pub async fn handle_api_key_provider(
|
||||
@@ -30,10 +34,11 @@ pub async fn handle_api_key_provider(
|
||||
provider: GenerationProvider,
|
||||
api_key: &str,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
match provider {
|
||||
GenerationProvider::Fal => {
|
||||
handle_fal(request, api_key, &app_env_configs.storyteller_host).await
|
||||
handle_fal(request, api_key, app_env_configs, storyteller_creds_manager).await
|
||||
}
|
||||
_ => {
|
||||
Err(GenerateError::NotYetImplemented(
|
||||
@@ -48,9 +53,11 @@ pub async fn handle_api_key_provider(
|
||||
async fn handle_fal(
|
||||
request: &TauriGenerateImageRequest,
|
||||
api_key: &str,
|
||||
api_host: &ApiHost,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?;
|
||||
let api_host = &app_env_configs.storyteller_host;
|
||||
|
||||
let router_model = tauri_image_model_to_router_model(tauri_model)
|
||||
.ok_or(GenerateError::NotYetImplemented(
|
||||
@@ -77,6 +84,13 @@ async fn handle_fal(
|
||||
idempotency_token: None,
|
||||
};
|
||||
|
||||
// Create a prompt record before sending the generation request.
|
||||
let maybe_prompt_token = create_prompt_record(
|
||||
&router_request,
|
||||
api_host,
|
||||
storyteller_creds_manager,
|
||||
).await;
|
||||
|
||||
let fal_client = RouterFalWebhookOptionalClient::from_str(api_key);
|
||||
let client = RouterClient::FalWebhookOptional(fal_client);
|
||||
|
||||
@@ -98,7 +112,7 @@ async fn handle_fal(
|
||||
|
||||
match request.send_request(&client).await {
|
||||
Ok(response) => {
|
||||
build_task_enqueue_success(tauri_model, response)
|
||||
build_task_enqueue_success(tauri_model, response, maybe_prompt_token)
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Fal image generation failed: {:?}", err);
|
||||
@@ -135,9 +149,39 @@ async fn resolve_image_inputs(
|
||||
Ok(Some(ImageListRef::Urls(urls)))
|
||||
}
|
||||
|
||||
/// Create a prompt record in the Artcraft backend before sending the generation request.
|
||||
/// Fails open: if prompt creation fails, we log and return None rather than blocking generation.
|
||||
async fn create_prompt_record(
|
||||
router_request: &GenerateImageRequestBuilder,
|
||||
api_host: &ApiHost,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Option<PromptToken> {
|
||||
let creds = match storyteller_creds_manager.get_credentials() {
|
||||
Ok(Some(creds)) => creds,
|
||||
_ => {
|
||||
warn!("[FalRouter] No Storyteller credentials available, skipping prompt creation");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let prompt_request = router_image_request_to_artcraft_prompt(router_request);
|
||||
|
||||
match create_prompt(api_host, Some(&creds), prompt_request).await {
|
||||
Ok(response) => {
|
||||
info!("[FalRouter] Created prompt: {:?}", response.prompt_token);
|
||||
Some(response.prompt_token)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("[FalRouter] Failed to create prompt (continuing anyway): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_task_enqueue_success(
|
||||
tauri_model: TauriImageModel,
|
||||
response: GenerateImageResponse,
|
||||
maybe_prompt_token: Option<PromptToken>,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let fal_payload = response.get_fal_payload()
|
||||
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
|
||||
@@ -155,5 +199,6 @@ fn build_task_enqueue_success(
|
||||
provider_job_id: Some(provider_job_id),
|
||||
maybe_queue_status_url: fal_payload.maybe_status_url,
|
||||
maybe_queue_response_url: fal_payload.maybe_response_url,
|
||||
maybe_prompt_token,
|
||||
})
|
||||
}
|
||||
+5
-3
@@ -3,13 +3,14 @@ use log::info;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::router::handle_api_providers::handle_api_key_provider;
|
||||
use crate::core::commands::generate::generate_image::providers::router::handle_web_login_providers::handle_web_login_provider;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft_router::handle_api_providers::handle_api_key_provider;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft_router::handle_web_login_providers::handle_web_login_provider;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::providers::credentials::payload::provider_credential_payload::ProviderCredentialPayload;
|
||||
use crate::core::providers::credentials::provider_credential_key::ProviderCredentialKey;
|
||||
use crate::core::providers::credentials::provider_credential_loading_cache::ProviderCredentialLoadingCache;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
|
||||
/// Dispatch an image generation request to a third-party provider via the router.
|
||||
///
|
||||
@@ -20,6 +21,7 @@ pub async fn handle_router(
|
||||
provider: GenerationProvider,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
credential_cache: &ProviderCredentialLoadingCache,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let credential_key = map_provider_to_credential_key(provider)?;
|
||||
|
||||
@@ -33,7 +35,7 @@ pub async fn handle_router(
|
||||
|
||||
match payload {
|
||||
ProviderCredentialPayload::ApiKey(api_key_data) => {
|
||||
handle_api_key_provider(request, provider, api_key_data.as_str(), app_env_configs).await
|
||||
handle_api_key_provider(request, provider, api_key_data.as_str(), app_env_configs, storyteller_creds_manager).await
|
||||
}
|
||||
ProviderCredentialPayload::WebLogin(web_login_data) => {
|
||||
handle_web_login_provider(request, provider, &web_login_data).await
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod artcraft;
|
||||
pub mod router;
|
||||
pub mod artcraft_router;
|
||||
+1
@@ -72,6 +72,7 @@ pub (super) async fn handle_artcraft_kling_1p6_pro(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ pub (super) async fn handle_artcraft_kling_2p1_master(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ pub (super) async fn handle_artcraft_kling_2p1_pro(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ pub (super) async fn handle_artcraft_kling_2p5_turbo_pro(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ pub (super) async fn handle_artcraft_kling_2p6_pro(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ pub (super) async fn handle_artcraft_seedance_1_lite(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ pub (super) async fn handle_artcraft_sora2(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ pub (super) async fn handle_artcraft_sora2_pro(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -70,6 +70,7 @@ pub (super) async fn handle_artcraft_veo2(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ pub (super) async fn handle_artcraft_veo3(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ pub (super) async fn handle_artcraft_veo3_fast(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ pub (super) async fn handle_artcraft_veo3p1(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ pub (super) async fn handle_artcraft_veo3p1_fast(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -80,6 +80,7 @@ pub(super) async fn handle_artcraft_video_via_router(
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -97,6 +97,7 @@ pub async fn handle_grok_video(
|
||||
provider_job_id: Some(post_id.to_string()),
|
||||
task_type: TaskType::VideoGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
+1
@@ -99,6 +99,7 @@ pub async fn handle_sora_sora2(
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::VideoGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod notify_frontend_of_completion;
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent;
|
||||
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
|
||||
use crate::core::events::functional_events::gaussian_generation_complete_event::{GaussianGenerationCompleteEvent, GeneratedGaussian};
|
||||
use crate::core::events::functional_events::image_edit_complete_event::{EditedImage, ImageEditCompleteEvent};
|
||||
use crate::core::events::functional_events::object_generation_complete_event::{GeneratedObject, ObjectGenerationCompleteEvent};
|
||||
use crate::core::events::functional_events::text_to_image_generation_complete_event::{GeneratedImage, TextToImageGenerationCompleteEvent};
|
||||
use crate::core::events::functional_events::video_generation_complete_event::{GeneratedVideo, VideoGenerationCompleteEvent};
|
||||
use artcraft_api_defs::utils::media_links_to_thumbnail_template::media_links_to_thumbnail_template;
|
||||
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
|
||||
use artcraft_client::endpoints::media_files::list_batch_generated_redux_media_files::list_batch_generated_redux_media_files;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use log::{error, info, warn};
|
||||
use sqlite_tasks::queries::task::Task;
|
||||
use tauri::AppHandle;
|
||||
use reqwest::Url;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Data about a successfully completed third-party generation.
|
||||
pub struct CompletionData {
|
||||
/// The primary media file token (first uploaded file).
|
||||
pub primary_media_file_token: MediaFileToken,
|
||||
|
||||
/// The CDN URL for the primary media file, if looked up.
|
||||
pub maybe_cdn_url: Option<Url>,
|
||||
|
||||
/// A thumbnail URL template for the primary media file, if available.
|
||||
pub maybe_thumbnail_url_template: Option<String>,
|
||||
|
||||
/// The batch token, if multiple files were generated.
|
||||
pub maybe_batch_token: Option<BatchGenerationToken>,
|
||||
|
||||
/// The media class of the generated output.
|
||||
pub media_class: TaskMediaFileClass,
|
||||
}
|
||||
|
||||
/// Notify the frontend that a third-party job has completed, sending the
|
||||
/// appropriate typed event based on the task type.
|
||||
///
|
||||
/// This is the cross-cutting equivalent of the Storyteller-specific
|
||||
/// `maybe_handle_frontend_caller_notification` — it works for any provider.
|
||||
pub async fn notify_frontend_of_completion(
|
||||
app: &AppHandle,
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
task: &Task,
|
||||
completion: &CompletionData,
|
||||
) {
|
||||
// Fire the generic generation-complete event (for the task queue UI).
|
||||
let generation_action = task_type_to_generation_action(task.task_type);
|
||||
let generation_model = task.model_type.and_then(task_model_type_to_generation_model);
|
||||
let generation_service = provider_to_generation_service(task.provider);
|
||||
|
||||
let complete_event = GenerationCompleteEvent {
|
||||
action: Some(generation_action),
|
||||
service: generation_service,
|
||||
model: generation_model,
|
||||
};
|
||||
complete_event.send_infallible(app);
|
||||
|
||||
// Fire the typed frontend notification (for the specific page/component that initiated the job).
|
||||
let result = match task.task_type {
|
||||
TaskType::ImageGeneration => {
|
||||
notify_image_generation(app, api_host, maybe_creds, task, completion).await
|
||||
}
|
||||
TaskType::ImageInpaintEdit => {
|
||||
notify_image_edit(app, api_host, maybe_creds, task, completion).await
|
||||
}
|
||||
TaskType::VideoGeneration => {
|
||||
notify_video_generation(app, task, completion)
|
||||
}
|
||||
TaskType::ObjectGeneration => {
|
||||
notify_object_generation(app, task, completion)
|
||||
}
|
||||
TaskType::GaussianGeneration => {
|
||||
notify_gaussian_generation(app, task, completion)
|
||||
}
|
||||
TaskType::BackgroundRemoval => {
|
||||
notify_background_removal(app, task, completion)
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("[ThirdPartyEvents] Failed to send frontend notification for task {}: {:?}", task.id.as_str(), err);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-type handlers ──
|
||||
|
||||
async fn notify_image_generation(
|
||||
app: &AppHandle,
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
task: &Task,
|
||||
completion: &CompletionData,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let generated_images = collect_generated_images(
|
||||
api_host,
|
||||
maybe_creds,
|
||||
completion,
|
||||
).await;
|
||||
|
||||
let event = TextToImageGenerationCompleteEvent {
|
||||
generated_images,
|
||||
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
|
||||
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
|
||||
};
|
||||
|
||||
event.send_infallible(app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify_image_edit(
|
||||
app: &AppHandle,
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
task: &Task,
|
||||
completion: &CompletionData,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let images = collect_generated_images(api_host, maybe_creds, completion).await;
|
||||
|
||||
let edited_images: Vec<EditedImage> = images.into_iter()
|
||||
.map(|img| EditedImage {
|
||||
media_token: img.media_token,
|
||||
cdn_url: img.cdn_url,
|
||||
maybe_thumbnail_template: img.maybe_thumbnail_template,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let event = ImageEditCompleteEvent {
|
||||
edited_images,
|
||||
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
|
||||
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
|
||||
};
|
||||
|
||||
event.send_infallible(app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_video_generation(
|
||||
app: &AppHandle,
|
||||
task: &Task,
|
||||
completion: &CompletionData,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let event = VideoGenerationCompleteEvent {
|
||||
generated_video: Some(GeneratedVideo {
|
||||
media_token: completion.primary_media_file_token.clone(),
|
||||
cdn_url: completion.maybe_cdn_url.clone().unwrap_or_else(|| Url::parse("https://cdn.artcraft.ai/placeholder").unwrap()),
|
||||
maybe_thumbnail_template: completion.maybe_thumbnail_url_template.clone(),
|
||||
}),
|
||||
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
|
||||
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
|
||||
};
|
||||
|
||||
event.send_infallible(app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_object_generation(
|
||||
app: &AppHandle,
|
||||
task: &Task,
|
||||
completion: &CompletionData,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let event = ObjectGenerationCompleteEvent {
|
||||
generated_object: Some(GeneratedObject {
|
||||
media_token: completion.primary_media_file_token.clone(),
|
||||
cdn_url: completion.maybe_cdn_url.clone().unwrap_or_else(|| Url::parse("https://cdn.artcraft.ai/placeholder").unwrap()),
|
||||
maybe_thumbnail_template: completion.maybe_thumbnail_url_template.clone(),
|
||||
}),
|
||||
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
|
||||
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
|
||||
};
|
||||
|
||||
event.send_infallible(app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_gaussian_generation(
|
||||
app: &AppHandle,
|
||||
task: &Task,
|
||||
completion: &CompletionData,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let event = GaussianGenerationCompleteEvent {
|
||||
generated_gaussian: Some(GeneratedGaussian {
|
||||
media_token: completion.primary_media_file_token.clone(),
|
||||
cdn_url: completion.maybe_cdn_url.clone().unwrap_or_else(|| Url::parse("https://cdn.artcraft.ai/placeholder").unwrap()),
|
||||
maybe_thumbnail_template: completion.maybe_thumbnail_url_template.clone(),
|
||||
}),
|
||||
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
|
||||
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
|
||||
};
|
||||
|
||||
event.send_infallible(app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_background_removal(
|
||||
app: &AppHandle,
|
||||
task: &Task,
|
||||
completion: &CompletionData,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let event = CanvasBackgroundRemovalCompleteEvent {
|
||||
media_token: completion.primary_media_file_token.clone(),
|
||||
image_cdn_url: completion.maybe_cdn_url.clone().unwrap_or_else(|| Url::parse("https://cdn.artcraft.ai/placeholder").unwrap()),
|
||||
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
|
||||
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
|
||||
};
|
||||
|
||||
event.send_infallible(app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
/// Collect generated images. If there's a batch token, fetch all files in the batch.
|
||||
/// Otherwise, return a single-element list with the primary file.
|
||||
async fn collect_generated_images(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
completion: &CompletionData,
|
||||
) -> Vec<GeneratedImage> {
|
||||
// If we have a batch token, try to fetch all files in the batch.
|
||||
if let Some(batch_token) = &completion.maybe_batch_token {
|
||||
match list_batch_generated_redux_media_files(api_host, maybe_creds, batch_token).await {
|
||||
Ok(result) if !result.media_files.is_empty() => {
|
||||
return result.media_files.into_iter()
|
||||
.map(|file| GeneratedImage {
|
||||
media_token: file.token,
|
||||
cdn_url: file.media_links.cdn_url,
|
||||
maybe_thumbnail_template: file.media_links.maybe_thumbnail_template,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Ok(_) => {
|
||||
warn!("[ThirdPartyEvents] Batch token {} returned no media files, falling back to primary", batch_token);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("[ThirdPartyEvents] Failed to list batch media files: {:?}, falling back to primary", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: single image from primary token.
|
||||
vec![GeneratedImage {
|
||||
media_token: completion.primary_media_file_token.clone(),
|
||||
cdn_url: completion.maybe_cdn_url.clone().unwrap_or_else(|| Url::parse("https://cdn.artcraft.ai/placeholder").unwrap()),
|
||||
maybe_thumbnail_template: completion.maybe_thumbnail_url_template.clone(),
|
||||
}]
|
||||
}
|
||||
|
||||
fn task_type_to_generation_action(task_type: TaskType) -> GenerationAction {
|
||||
match task_type {
|
||||
TaskType::ImageGeneration => GenerationAction::GenerateImage,
|
||||
TaskType::VideoGeneration => GenerationAction::GenerateVideo,
|
||||
TaskType::BackgroundRemoval => GenerationAction::RemoveBackground,
|
||||
TaskType::ObjectGeneration => GenerationAction::ImageTo3d,
|
||||
TaskType::GaussianGeneration => GenerationAction::GenerateGaussian,
|
||||
TaskType::ImageInpaintEdit => GenerationAction::ImageInpaintEdit,
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_to_generation_service(provider: GenerationProvider) -> GenerationServiceProvider {
|
||||
match provider {
|
||||
GenerationProvider::Artcraft => GenerationServiceProvider::Artcraft,
|
||||
GenerationProvider::Fal => GenerationServiceProvider::Fal,
|
||||
GenerationProvider::Grok => GenerationServiceProvider::Grok,
|
||||
GenerationProvider::Midjourney => GenerationServiceProvider::Midjourney,
|
||||
GenerationProvider::Sora => GenerationServiceProvider::Sora,
|
||||
GenerationProvider::WorldLabs => GenerationServiceProvider::WorldLabs,
|
||||
}
|
||||
}
|
||||
|
||||
fn task_model_type_to_generation_model(model: TaskModelType) -> Option<GenerationModel> {
|
||||
match model {
|
||||
TaskModelType::Flux1Dev => Some(GenerationModel::Flux1Dev),
|
||||
TaskModelType::Flux1Schnell => Some(GenerationModel::Flux1Schnell),
|
||||
TaskModelType::FluxPro1 => Some(GenerationModel::FluxPro1),
|
||||
TaskModelType::FluxPro11 => Some(GenerationModel::FluxPro11),
|
||||
TaskModelType::FluxPro11Ultra => Some(GenerationModel::FluxPro11Ultra),
|
||||
TaskModelType::FluxProKontextMax => Some(GenerationModel::FluxProKontextMax),
|
||||
TaskModelType::FluxDevJuggernaut => Some(GenerationModel::FluxDevJuggernaut),
|
||||
TaskModelType::Flux2LoraAngles => Some(GenerationModel::Flux2LoraAngles),
|
||||
TaskModelType::GptImage1 => Some(GenerationModel::GptImage1),
|
||||
TaskModelType::GptImage1p5 => Some(GenerationModel::GptImage1p5),
|
||||
TaskModelType::GptImage2 => Some(GenerationModel::GptImage2),
|
||||
TaskModelType::NanoBanana => Some(GenerationModel::NanoBanana),
|
||||
TaskModelType::NanoBanana2 => Some(GenerationModel::NanoBanana2),
|
||||
TaskModelType::NanoBananaPro => Some(GenerationModel::NanoBananaPro),
|
||||
TaskModelType::Seedream4 => Some(GenerationModel::Seedream4),
|
||||
TaskModelType::Seedream4p5 => Some(GenerationModel::Seedream4p5),
|
||||
TaskModelType::Seedream5Lite => Some(GenerationModel::Seedream5Lite),
|
||||
TaskModelType::QwenEdit2511Angles => Some(GenerationModel::QwenEdit2511Angles),
|
||||
TaskModelType::GrokImage => Some(GenerationModel::GrokImage),
|
||||
TaskModelType::Recraft3 => Some(GenerationModel::Recraft3),
|
||||
TaskModelType::GrokVideo => Some(GenerationModel::GrokVideo),
|
||||
TaskModelType::Kling16Pro => Some(GenerationModel::Kling1_6),
|
||||
TaskModelType::Kling21Pro => Some(GenerationModel::Kling21Pro),
|
||||
TaskModelType::Kling21Master => Some(GenerationModel::Kling21Master),
|
||||
TaskModelType::Kling2p5TurboPro => Some(GenerationModel::Kling2p5TurboPro),
|
||||
TaskModelType::Kling2p6Pro => Some(GenerationModel::Kling2p6Pro),
|
||||
TaskModelType::Kling3p0Standard => Some(GenerationModel::Kling3p0Standard),
|
||||
TaskModelType::Kling3p0Pro => Some(GenerationModel::Kling3p0Pro),
|
||||
TaskModelType::HappyHorse1p0 => Some(GenerationModel::HappyHorse1p0),
|
||||
TaskModelType::Seedance10Lite => Some(GenerationModel::Seedance10Lite),
|
||||
TaskModelType::Seedance1p5Pro => Some(GenerationModel::Seedance1p5Pro),
|
||||
TaskModelType::Seedance2p0 => Some(GenerationModel::Seedance2p0),
|
||||
TaskModelType::Seedance2p0Fast => Some(GenerationModel::Seedance2p0Fast),
|
||||
TaskModelType::Sora2 => Some(GenerationModel::Sora2),
|
||||
TaskModelType::Sora2Pro => Some(GenerationModel::Sora2Pro),
|
||||
TaskModelType::Veo2 => Some(GenerationModel::Veo2),
|
||||
TaskModelType::Veo3 => Some(GenerationModel::Veo3),
|
||||
TaskModelType::Veo3Fast => Some(GenerationModel::Veo3Fast),
|
||||
TaskModelType::Veo3p1 => Some(GenerationModel::Veo3p1),
|
||||
TaskModelType::Veo3p1Fast => Some(GenerationModel::Veo3p1Fast),
|
||||
TaskModelType::Hunyuan3d2_0 => Some(GenerationModel::Hunyuan3d2_0),
|
||||
TaskModelType::Hunyuan3d2_1 => Some(GenerationModel::Hunyuan3d2_1),
|
||||
TaskModelType::Hunyuan3d3 => Some(GenerationModel::Hunyuan3d3),
|
||||
TaskModelType::Midjourney => Some(GenerationModel::Midjourney),
|
||||
TaskModelType::Gemini25Flash => Some(GenerationModel::Gemini25Flash),
|
||||
TaskModelType::WorldlabsMarble => Some(GenerationModel::WorldlabsMarble),
|
||||
TaskModelType::WorldlabsMarble0p1Mini => Some(GenerationModel::WorldlabsMarble0p1Mini),
|
||||
TaskModelType::WorldlabsMarble0p1Plus => Some(GenerationModel::WorldlabsMarble0p1Plus),
|
||||
}
|
||||
}
|
||||
+53
-48
@@ -1,12 +1,11 @@
|
||||
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_complete_event::GenerationCompleteEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::threads::third_party_task_polling_thread::events::notify_frontend_of_completion::{
|
||||
notify_frontend_of_completion, CompletionData,
|
||||
};
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_api_defs::prompts::create_prompt::CreatePromptRequest;
|
||||
use artcraft_api_defs::utils::media_links_to_thumbnail_template::media_links_to_thumbnail_template;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::error::api_error::ApiError;
|
||||
@@ -21,7 +20,6 @@ use artcraft_client::endpoints::media_files::upload_image_media_file_from_file::
|
||||
use artcraft_client::endpoints::media_files::upload_video_media_file_from_file::{
|
||||
upload_video_media_file_from_file, UploadVideoFromFileArgs,
|
||||
};
|
||||
use artcraft_client::endpoints::prompts::create_prompt::create_prompt;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
@@ -39,6 +37,7 @@ use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
|
||||
pub async fn handle_fal_complete(
|
||||
@@ -82,7 +81,7 @@ async fn handle_fal_complete_inner(
|
||||
let extracted = job_response.extracted_contents;
|
||||
|
||||
// Determine what kind of media we got and collect download URLs.
|
||||
let (urls, media_class, generation_action) = collect_media_urls(task, &extracted)?;
|
||||
let (urls, media_class) = collect_media_urls(task, &extracted)?;
|
||||
|
||||
if urls.is_empty() {
|
||||
warn!("[FalComplete] Task {} completed but no downloadable media found in response", task.id.as_str());
|
||||
@@ -90,26 +89,15 @@ async fn handle_fal_complete_inner(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a prompt record
|
||||
let prompt_response = create_prompt(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
CreatePromptRequest {
|
||||
uuid_idempotency_token: generate_random_uuid(),
|
||||
positive_prompt: None,
|
||||
negative_prompt: None,
|
||||
model_type: None,
|
||||
generation_provider: Some(GenerationProvider::Fal),
|
||||
maybe_generation_mode: None,
|
||||
maybe_aspect_ratio: None,
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: None,
|
||||
maybe_generate_audio: None,
|
||||
maybe_duration_seconds: None,
|
||||
},
|
||||
).await?;
|
||||
// Use the prompt token from the task record (created at generation time).
|
||||
let maybe_prompt_token = task.prompt_token.as_ref()
|
||||
.map(|s| PromptToken::new_from_str(s));
|
||||
|
||||
info!("[FalComplete] Created prompt: {:?}", prompt_response.prompt_token);
|
||||
if maybe_prompt_token.is_some() {
|
||||
info!("[FalComplete] Using prompt token from task: {:?}", maybe_prompt_token);
|
||||
} else {
|
||||
warn!("[FalComplete] Task {} has no prompt token, uploading without prompt association", task.id.as_str());
|
||||
}
|
||||
|
||||
let maybe_batch_token = if urls.len() > 1 {
|
||||
let token = BatchGenerationToken::generate();
|
||||
@@ -132,7 +120,7 @@ async fn handle_fal_complete_inner(
|
||||
&creds,
|
||||
app_env_configs,
|
||||
&download_path,
|
||||
&prompt_response.prompt_token,
|
||||
maybe_prompt_token.as_ref(),
|
||||
maybe_batch_token.as_ref(),
|
||||
media_class,
|
||||
).await?;
|
||||
@@ -145,13 +133,15 @@ async fn handle_fal_complete_inner(
|
||||
}
|
||||
|
||||
// Look up CDN/thumbnail URLs for the primary media file
|
||||
let mut maybe_cdn_url = None;
|
||||
let mut maybe_cdn_url: Option<reqwest::Url> = None;
|
||||
let mut maybe_cdn_url_str: Option<String> = None;
|
||||
let mut maybe_thumbnail_url_template = None;
|
||||
|
||||
if let Some(media_file_token) = maybe_primary_media_file_token.as_ref() {
|
||||
match get_media_file(&app_env_configs.storyteller_host, media_file_token).await {
|
||||
Ok(response) => {
|
||||
maybe_cdn_url = Some(response.media_file.media_links.cdn_url.to_string());
|
||||
maybe_cdn_url = Some(response.media_file.media_links.cdn_url.clone());
|
||||
maybe_cdn_url_str = Some(response.media_file.media_links.cdn_url.to_string());
|
||||
maybe_thumbnail_url_template = media_links_to_thumbnail_template(&response.media_file.media_links)
|
||||
.map(|s| s.to_string());
|
||||
}
|
||||
@@ -161,24 +151,35 @@ async fn handle_fal_complete_inner(
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the task as completed
|
||||
// Mark the task as completed in the local database
|
||||
let updated = update_successful_task_status_with_metadata(UpdateSuccessfulTaskArgs {
|
||||
db: task_database.get_connection(),
|
||||
task_id: &task.id,
|
||||
maybe_batch_token: maybe_batch_token.as_ref(),
|
||||
maybe_primary_media_file_token: maybe_primary_media_file_token.as_ref(),
|
||||
maybe_primary_media_file_class: Some(media_class),
|
||||
maybe_primary_media_file_cdn_url: maybe_cdn_url.as_deref(),
|
||||
maybe_primary_media_file_cdn_url: maybe_cdn_url_str.as_deref(),
|
||||
maybe_primary_media_file_thumbnail_url_template: maybe_thumbnail_url_template.as_deref(),
|
||||
}).await?;
|
||||
|
||||
if updated {
|
||||
let event = GenerationCompleteEvent {
|
||||
action: Some(generation_action),
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
event.send_infallible(app_handle);
|
||||
if let Some(primary_token) = maybe_primary_media_file_token {
|
||||
let completion = CompletionData {
|
||||
primary_media_file_token: primary_token,
|
||||
maybe_cdn_url,
|
||||
maybe_thumbnail_url_template,
|
||||
maybe_batch_token,
|
||||
media_class,
|
||||
};
|
||||
|
||||
notify_frontend_of_completion(
|
||||
app_handle,
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
task,
|
||||
&completion,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
info!("[FalComplete] Task {} fully handled", task.id.as_str());
|
||||
@@ -192,10 +193,10 @@ async fn handle_fal_complete_inner(
|
||||
fn collect_media_urls(
|
||||
task: &Task,
|
||||
extracted: &Option<PollResponseExtractedContents>,
|
||||
) -> Result<(Vec<String>, TaskMediaFileClass, GenerationAction), Box<dyn std::error::Error>> {
|
||||
) -> Result<(Vec<String>, TaskMediaFileClass), Box<dyn std::error::Error>> {
|
||||
let extracted = match extracted {
|
||||
Some(e) => e,
|
||||
None => return Ok((vec![], TaskMediaFileClass::Image, GenerationAction::GenerateImage)),
|
||||
None => return Ok((vec![], TaskMediaFileClass::Image)),
|
||||
};
|
||||
|
||||
// Images (batch)
|
||||
@@ -204,32 +205,32 @@ fn collect_media_urls(
|
||||
.filter_map(|img| img.url.clone())
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
return Ok((urls, TaskMediaFileClass::Image, GenerationAction::GenerateImage));
|
||||
return Ok((urls, TaskMediaFileClass::Image));
|
||||
}
|
||||
}
|
||||
|
||||
// Single image (e.g. background removal)
|
||||
if let Some(image) = &extracted.image {
|
||||
if let Some(url) = &image.url {
|
||||
return Ok((vec![url.clone()], TaskMediaFileClass::Image, GenerationAction::GenerateImage));
|
||||
return Ok((vec![url.clone()], TaskMediaFileClass::Image));
|
||||
}
|
||||
}
|
||||
|
||||
// Video
|
||||
if let Some(video) = &extracted.video {
|
||||
if let Some(url) = &video.url {
|
||||
return Ok((vec![url.clone()], TaskMediaFileClass::Video, GenerationAction::GenerateVideo));
|
||||
return Ok((vec![url.clone()], TaskMediaFileClass::Video));
|
||||
}
|
||||
}
|
||||
|
||||
// 3D model (GLB)
|
||||
if let Some(glb) = &extracted.model_glb {
|
||||
if let Some(url) = &glb.url {
|
||||
return Ok((vec![url.clone()], TaskMediaFileClass::Dimensional, GenerationAction::ImageTo3d));
|
||||
return Ok((vec![url.clone()], TaskMediaFileClass::Dimensional));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((vec![], TaskMediaFileClass::Image, GenerationAction::GenerateImage))
|
||||
Ok((vec![], TaskMediaFileClass::Image))
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
@@ -261,14 +262,14 @@ async fn upload_to_backend(
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
download_path: &PathBuf,
|
||||
prompt_token: &tokens::tokens::prompts::PromptToken,
|
||||
maybe_prompt_token: Option<&PromptToken>,
|
||||
maybe_batch_token: Option<&BatchGenerationToken>,
|
||||
media_class: TaskMediaFileClass,
|
||||
) -> Result<MediaFileToken, Box<dyn std::error::Error>> {
|
||||
let mut retry_delay_secs = INITIAL_RETRY_DELAY_SECS;
|
||||
|
||||
for attempt in 0..MAX_UPLOAD_RETRIES {
|
||||
let result = try_upload(creds, app_env_configs, download_path, prompt_token, maybe_batch_token, media_class).await;
|
||||
let result = try_upload(creds, app_env_configs, download_path, maybe_prompt_token, maybe_batch_token, media_class).await;
|
||||
|
||||
match result {
|
||||
Ok(token) => return Ok(token),
|
||||
@@ -300,7 +301,7 @@ async fn try_upload(
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
download_path: &PathBuf,
|
||||
prompt_token: &tokens::tokens::prompts::PromptToken,
|
||||
maybe_prompt_token: Option<&PromptToken>,
|
||||
maybe_batch_token: Option<&BatchGenerationToken>,
|
||||
media_class: TaskMediaFileClass,
|
||||
) -> Result<MediaFileToken, StorytellerError> {
|
||||
@@ -310,7 +311,8 @@ async fn try_upload(
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(creds),
|
||||
path: download_path,
|
||||
maybe_prompt_token: Some(prompt_token),
|
||||
maybe_prompt_token,
|
||||
maybe_generation_provider: Some(GenerationProvider::Fal),
|
||||
}).await?;
|
||||
result.media_file_token
|
||||
}
|
||||
@@ -319,6 +321,7 @@ async fn try_upload(
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(creds),
|
||||
path: download_path,
|
||||
maybe_generation_provider: Some(GenerationProvider::Fal),
|
||||
}).await?;
|
||||
result.media_file_token
|
||||
}
|
||||
@@ -328,8 +331,9 @@ async fn try_upload(
|
||||
maybe_creds: Some(creds),
|
||||
path: download_path,
|
||||
is_intermediate_system_file: false,
|
||||
maybe_prompt_token: Some(prompt_token),
|
||||
maybe_prompt_token,
|
||||
maybe_batch_token,
|
||||
maybe_generation_provider: Some(GenerationProvider::Fal),
|
||||
}).await?;
|
||||
result.media_file_token
|
||||
}
|
||||
@@ -337,3 +341,4 @@ async fn try_upload(
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod third_party_task_polling_thread;
|
||||
pub mod events;
|
||||
pub mod handlers;
|
||||
pub mod third_party_task_polling_thread;
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ pub async fn upload_image_bytes_as_media_file(
|
||||
image_bytes,
|
||||
image_type: ImageType::Png,
|
||||
is_intermediate_system_file: false,
|
||||
maybe_generation_provider: None,
|
||||
}).await.map_err(|err| {
|
||||
log::error!("Failed to upload image bytes as media file: {:?}", err);
|
||||
GenerateError::from(err)
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ pub async fn upload_mask_image_bytes_as_media_file(
|
||||
image_bytes: normalized.0,
|
||||
image_type: ImageType::Png,
|
||||
is_intermediate_system_file: true,
|
||||
maybe_generation_provider: None,
|
||||
}).await.map_err(|err| {
|
||||
error!("Failed to upload mask image bytes: {:?}", err);
|
||||
GenerateError::from(err)
|
||||
|
||||
+1
@@ -219,6 +219,7 @@ async fn upload_images_to_storyteller(
|
||||
is_intermediate_system_file: false,
|
||||
maybe_prompt_token: Some(&prompt_response.prompt_token),
|
||||
maybe_batch_token: Some(&batch_token),
|
||||
maybe_generation_provider: Some(GenerationProvider::Grok),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
|
||||
+1
@@ -277,6 +277,7 @@ async fn upload_grok_video(
|
||||
maybe_creds: Some(&storyteller_creds),
|
||||
path: &download_path,
|
||||
maybe_prompt_token: Some(&prompt_response.prompt_token),
|
||||
maybe_generation_provider: Some(GenerationProvider::Grok),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
|
||||
+1
@@ -309,6 +309,7 @@ async fn upload_midjourney_batch(
|
||||
is_intermediate_system_file: false,
|
||||
maybe_prompt_token: Some(&prompt_response.prompt_token),
|
||||
maybe_batch_token: Some(&batch_token),
|
||||
maybe_generation_provider: Some(GenerationProvider::Midjourney),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
|
||||
+3
@@ -6,6 +6,7 @@ use artcraft_client::credentials::storyteller_credential_set::StorytellerCredent
|
||||
use artcraft_client::endpoints::media_files::upload_image_media_file_from_file::{upload_image_media_file_from_file, UploadImageFromFileArgs};
|
||||
use artcraft_client::endpoints::media_files::upload_video_media_file_from_file::{upload_video_media_file_from_file, UploadVideoFromFileArgs};
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
@@ -47,6 +48,7 @@ pub(super) async fn upload_generation_to_backend<P: AsRef<Path>>(args: UploadGen
|
||||
is_intermediate_system_file: false,
|
||||
maybe_prompt_token: args.maybe_prompt_token,
|
||||
maybe_batch_token: None, // TODO: This should be added soon.
|
||||
maybe_generation_provider: Some(GenerationProvider::Sora),
|
||||
}).await?;
|
||||
|
||||
info!("Uploaded image to API backend: {:?}", result.media_file_token);
|
||||
@@ -58,6 +60,7 @@ pub(super) async fn upload_generation_to_backend<P: AsRef<Path>>(args: UploadGen
|
||||
maybe_creds: Some(&args.storyteller_creds),
|
||||
path: args.upload_path,
|
||||
maybe_prompt_token: args.maybe_prompt_token,
|
||||
maybe_generation_provider: Some(GenerationProvider::Sora),
|
||||
}).await?;
|
||||
|
||||
info!("Uploaded video to API backend: {:?}", result.media_file_token);
|
||||
|
||||
+1
@@ -266,6 +266,7 @@ async fn upload_spz_splat(
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(&storyteller_creds),
|
||||
path: &spz_download_filename,
|
||||
maybe_generation_provider: Some(GenerationProvider::WorldLabs),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
|
||||
@@ -5,6 +5,7 @@ use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
pub struct CreateTaskArgs<'a> {
|
||||
@@ -16,6 +17,7 @@ pub struct CreateTaskArgs<'a> {
|
||||
pub provider_job_id: Option<&'a str>,
|
||||
pub queue_status_url: Option<&'a str>,
|
||||
pub queue_response_url: Option<&'a str>,
|
||||
pub prompt_token: Option<&'a PromptToken>,
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
pub frontend_subscriber_id: Option<&'a str>,
|
||||
pub frontend_subscriber_payload: Option<&'a str>,
|
||||
@@ -32,6 +34,7 @@ pub async fn create_task(
|
||||
let task_type_temp = args.task_type.to_str();
|
||||
let model_type_temp = args.model_type.map(|s| s.to_str());
|
||||
let provider_temp = args.provider.to_string();
|
||||
let prompt_token_temp = args.prompt_token.map(|t| t.as_str());
|
||||
let frontend_caller_temp = args.frontend_caller.map(|s| s.to_str());
|
||||
|
||||
let query = sqlx::query!(r#"
|
||||
@@ -44,11 +47,12 @@ pub async fn create_task(
|
||||
provider_job_id,
|
||||
queue_status_url,
|
||||
queue_response_url,
|
||||
prompt_token,
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
task_id_temp,
|
||||
status_temp,
|
||||
@@ -58,6 +62,7 @@ pub async fn create_task(
|
||||
args.provider_job_id,
|
||||
args.queue_status_url,
|
||||
args.queue_response_url,
|
||||
prompt_token_temp,
|
||||
frontend_caller_temp,
|
||||
args.frontend_subscriber_id,
|
||||
args.frontend_subscriber_payload
|
||||
|
||||
+2
@@ -35,6 +35,7 @@ pub async fn get_task_by_provider_and_provider_job_id(
|
||||
frontend_caller,
|
||||
queue_status_url,
|
||||
queue_response_url,
|
||||
prompt_token,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
FROM tasks
|
||||
@@ -73,6 +74,7 @@ pub async fn get_task_by_provider_and_provider_job_id(
|
||||
provider_job_id: record.provider_job_id,
|
||||
queue_status_url: record.queue_status_url,
|
||||
queue_response_url: record.queue_response_url,
|
||||
prompt_token: record.prompt_token,
|
||||
frontend_caller: record.frontend_caller
|
||||
.map(|caller| TauriCommandCaller::from_str(&caller))
|
||||
.transpose()?,
|
||||
|
||||
@@ -35,6 +35,7 @@ pub async fn list_non_artcraft_pending_tasks(
|
||||
provider_job_id,
|
||||
queue_status_url,
|
||||
queue_response_url,
|
||||
prompt_token,
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
@@ -70,6 +71,7 @@ pub async fn list_non_artcraft_pending_tasks(
|
||||
provider_job_id: task.provider_job_id,
|
||||
queue_status_url: task.queue_status_url,
|
||||
queue_response_url: task.queue_response_url,
|
||||
prompt_token: task.prompt_token,
|
||||
frontend_caller: task.frontend_caller
|
||||
.map(|caller| TauriCommandCaller::from_str(&caller))
|
||||
.transpose()?,
|
||||
|
||||
@@ -34,6 +34,7 @@ pub async fn list_tasks_by_provider_and_status(
|
||||
provider_job_id,
|
||||
queue_status_url,
|
||||
queue_response_url,
|
||||
prompt_token,
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
@@ -77,6 +78,7 @@ pub async fn list_tasks_by_provider_and_status(
|
||||
provider_job_id: task.provider_job_id,
|
||||
queue_status_url: task.queue_status_url,
|
||||
queue_response_url: task.queue_response_url,
|
||||
prompt_token: task.prompt_token,
|
||||
frontend_caller: task.frontend_caller
|
||||
.map(|caller| TauriCommandCaller::from_str(&caller))
|
||||
.transpose()?,
|
||||
|
||||
@@ -34,6 +34,7 @@ pub async fn list_tasks_by_provider_and_tokens(
|
||||
frontend_caller,
|
||||
queue_status_url,
|
||||
queue_response_url,
|
||||
prompt_token,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
FROM tasks
|
||||
@@ -76,6 +77,7 @@ pub async fn list_tasks_by_provider_and_tokens(
|
||||
provider_job_id: task.provider_job_id,
|
||||
queue_status_url: task.queue_status_url,
|
||||
queue_response_url: task.queue_response_url,
|
||||
prompt_token: task.prompt_token,
|
||||
frontend_caller: task.frontend_caller
|
||||
.map(|caller| TauriCommandCaller::from_str(&caller))
|
||||
.transpose()?,
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct Task {
|
||||
pub provider_job_id: Option<String>,
|
||||
pub queue_status_url: Option<String>,
|
||||
pub queue_response_url: Option<String>,
|
||||
pub prompt_token: Option<String>,
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
@@ -31,6 +32,7 @@ pub (crate) struct RawTask {
|
||||
pub (crate) provider_job_id: Option<String>,
|
||||
pub (crate) queue_status_url: Option<String>,
|
||||
pub (crate) queue_response_url: Option<String>,
|
||||
pub (crate) prompt_token: Option<String>,
|
||||
pub (crate) frontend_caller: Option<String>,
|
||||
pub (crate) frontend_subscriber_id: Option<String>,
|
||||
pub (crate) frontend_subscriber_payload: Option<String>,
|
||||
|
||||
Generated
+86
-168
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user