mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-27 12:10:08 -04:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f960a5d4 | ||
|
|
3de468389e | ||
|
|
a609282d8a | ||
|
|
0a32e06cd5 | ||
|
|
f1563feb91 | ||
|
|
cc73ffab5d | ||
|
|
d374d9339c | ||
|
|
53681eac3f | ||
|
|
f22369ea4a | ||
|
|
f7060c0278 | ||
|
|
2af8fbd9e1 | ||
|
|
a4eb74292f | ||
|
|
78b8c56066 | ||
|
|
221aa347bf | ||
|
|
14a27ee803 | ||
|
|
36e35dab0b |
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 13
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0fa824822cd94024dce21243f41e69e4a3e8685c4de676834a7c0ee14982c976"
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status: enums::common::job_status_plus::JobStatusPlus",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 76
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "external_third_party_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "64dfacc1f0552d539f009fea91e902a2fe296c06ad539808596bbf2c8fc9eaa0"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = ?,\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = NULL,\n success_inference_execution_millis = NULL,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f68a8917e639418a0bb952bef606c96299001e143c745ee30df99b72d1235d35"
|
||||
}
|
||||
Generated
+26
-3
@@ -689,6 +689,19 @@ dependencies = [
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "artcraft_api_defs"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"enums",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tokens",
|
||||
"utoipa",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "0.9.3"
|
||||
@@ -3554,6 +3567,8 @@ dependencies = [
|
||||
"fal-derive",
|
||||
"futures 0.3.31",
|
||||
"image",
|
||||
"log",
|
||||
"percent-encoding-rfc3986",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7230,6 +7245,12 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding-rfc3986"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3637c05577168127568a64e9dc5a6887da720efef07b3d9472d45f63ab191166"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.8.0"
|
||||
@@ -9876,12 +9897,14 @@ dependencies = [
|
||||
"actix_cors_configs",
|
||||
"actix_helpers",
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"async-stripe",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"billing_component",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"bytes 1.10.1",
|
||||
"chrono",
|
||||
"cloud_storage",
|
||||
"collections",
|
||||
@@ -9899,6 +9922,7 @@ dependencies = [
|
||||
"enums",
|
||||
"enums_public",
|
||||
"errors",
|
||||
"fal_client",
|
||||
"filesys",
|
||||
"futures 0.1.31",
|
||||
"futures 0.3.31",
|
||||
@@ -10796,11 +10820,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.18.0"
|
||||
version = "3.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
|
||||
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"fastrand 2.3.0",
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
|
||||
@@ -88,6 +88,7 @@ members = [
|
||||
#"crates/schema/database/sqlite_queries",
|
||||
|
||||
# Schema (public): common strongly-typed tokens, ids, payloads, etc.
|
||||
"crates/schema/public/api/artcraft_api_defs",
|
||||
"crates/schema/public/composite_identifiers",
|
||||
"crates/schema/public/enums",
|
||||
"crates/schema/public/enums_public",
|
||||
@@ -181,6 +182,7 @@ actix-web-actors = "4.3.1+deprecated"
|
||||
actix-web-lab = "0.20.2"
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.22.1"
|
||||
bytes = "1.10.1"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
cookie = "0.18.1"
|
||||
futures = { version = "0.3.31" }
|
||||
@@ -197,18 +199,22 @@ serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34+deprecated"
|
||||
sha3 = "0.10.8"
|
||||
tauri = { version = "2.4.1", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution
|
||||
tempdir = "0.3.7"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.45.1", features = ["macros"] }
|
||||
toml = "0.8.22"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
|
||||
# Internal libs
|
||||
artcraft_api_defs = { path = "crates/schema/public/api/artcraft_api_defs" }
|
||||
build_metadata = { path = "crates/lib/build_metadata" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
enums_public = { path = "crates/schema/public/enums_public" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
fal_client = { path = "crates/lib/api_clients/fal_client" }
|
||||
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
idempotency = { path = "crates/lib/idempotency" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
|
||||
@@ -39,6 +39,9 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- Examples (may not be up to date):
|
||||
-- * text_to_speech
|
||||
-- * voice_conversion
|
||||
-- * image_generation
|
||||
-- * video_generation
|
||||
-- * background_removal
|
||||
inference_category VARCHAR(32) NOT NULL,
|
||||
|
||||
-- If the job is externally enqueued, this is the third-party service
|
||||
|
||||
@@ -54,8 +54,8 @@ serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-log = "2.3.1"
|
||||
tauri-plugin-upload = "2.2.1"
|
||||
tempdir = "0.3.7"
|
||||
tempfile = "3.18.0"
|
||||
tempdir = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
url = { workspace = true }
|
||||
|
||||
+2
-3
@@ -24,9 +24,8 @@ use base64::prelude::BASE64_STANDARD;
|
||||
use base64::{DecodeError, Engine};
|
||||
use errors::{AnyhowError, AnyhowResult};
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use fal_client::requests::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use fal_client::requests::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use fal_client::requests::remove_background_rembg::remove_background_rembg;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, EncodableLayout, ImageReader};
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use fal_client::requests::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use log::{error, info, warn};
|
||||
use tauri::AppHandle;
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ use crate::services::sora::state::sora_credential_manager::SoraCredentialManager
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use fal_client::requests::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use fal_client::requests::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use log::{error, info, warn};
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
|
||||
@@ -24,7 +24,7 @@ use base64::prelude::BASE64_STANDARD;
|
||||
use base64::{DecodeError, Engine};
|
||||
use errors::{AnyhowError, AnyhowResult};
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use fal_client::requests::remove_background_rembg::remove_background_rembg;
|
||||
use fal_client::requests::queue::remove_background_rembg::remove_background_rembg;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, EncodableLayout, ImageReader};
|
||||
|
||||
+1
-2
@@ -24,8 +24,7 @@ use base64::prelude::BASE64_STANDARD;
|
||||
use base64::{DecodeError, Engine};
|
||||
use errors::{AnyhowError, AnyhowResult};
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use fal_client::requests::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
|
||||
use fal_client::requests::remove_background_rembg::remove_background_rembg;
|
||||
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, EncodableLayout, ImageReader};
|
||||
|
||||
+1
-2
@@ -24,8 +24,7 @@ use base64::prelude::BASE64_STANDARD;
|
||||
use base64::{DecodeError, Engine};
|
||||
use errors::{AnyhowError, AnyhowResult};
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use fal_client::requests::remove_background_rembg::remove_background_rembg;
|
||||
use fal_client::requests::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
|
||||
use fal_client::requests::queue::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, EncodableLayout, ImageReader};
|
||||
|
||||
@@ -14,4 +14,5 @@ pub fn add_artcraft_3d(cors: Cors, _is_production: bool) -> Cors {
|
||||
.allowed_origin("http://localhost:5173")
|
||||
.allowed_origin("http://localhost:5174") // If already started
|
||||
.allowed_origin("http://localhost:5175") // If already started
|
||||
.allowed_origin("https://macaroni-1.tailce84f.ts.net") // TODO
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ path = "src/lib.rs"
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
file_to_base64_url = { path = "../../files/file_to_base64_url" }
|
||||
file_to_base64_url = { workspace = true }
|
||||
errors = { workspace = true }
|
||||
|
||||
# External
|
||||
|
||||
@@ -15,6 +15,8 @@ pub enum FalErrorPlus {
|
||||
UrlParseError(url::ParseError),
|
||||
/// An endpoint we don't support yet.
|
||||
UnhandledEndpoint(String),
|
||||
/// Error from the `reqwest` crate.
|
||||
ReqwestError(reqwest::Error),
|
||||
}
|
||||
|
||||
impl Display for FalErrorPlus {
|
||||
@@ -25,6 +27,7 @@ impl Display for FalErrorPlus {
|
||||
Self::AnyhowError(err) => write!(f, "FalErrorPlus::AnyhowError: {:?}", err),
|
||||
Self::UrlParseError(err) => write!(f, "FalErrorPlus::UrlParseError: {:?}", err),
|
||||
Self::UnhandledEndpoint(endpoint) => write!(f, "FalErrorPlus::UnhandledEndpoint: {:?}", endpoint),
|
||||
Self::ReqwestError(err) => write!(f, "FalErrorPlus::ReqwestError: {:?}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,3 +51,9 @@ impl From<url::ParseError> for FalErrorPlus {
|
||||
FalErrorPlus::UrlParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for FalErrorPlus {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
FalErrorPlus::ReqwestError(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
pub mod enqueue_hunyuan2_image_to_3d;
|
||||
pub mod remove_background_rembg;
|
||||
pub mod video_gen;
|
||||
pub mod image_gen;
|
||||
pub mod queue;
|
||||
pub mod webhook;
|
||||
+1
-1
@@ -42,7 +42,7 @@ pub async fn enqueue_hunyuan2_image_to_3d<P: AsRef<Path>>(args: Hunyuan2Args<'_,
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
|
||||
use crate::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
+1
-1
@@ -42,7 +42,7 @@ pub async fn enqueue_flux_pro_ultra_text_to_image(args: FluxProUltraTextToImageA
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use crate::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ pub async fn enqueue_recraft3_text_to_image(args: Recraft3TextToImageArgs<'_>) -
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use crate::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod enqueue_hunyuan2_image_to_3d;
|
||||
pub mod image_gen;
|
||||
pub mod remove_background_rembg;
|
||||
pub mod video_gen;
|
||||
+1
-6
@@ -9,11 +9,6 @@ use futures::StreamExt;
|
||||
use std::path::Path;
|
||||
use url::Url;
|
||||
|
||||
pub struct RemBgArgs<'a, P: AsRef<Path>> {
|
||||
pub image_path: P,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
pub struct RemBgResponse {
|
||||
pub image_url: Url,
|
||||
}
|
||||
@@ -59,7 +54,7 @@ pub async fn remove_background_rembg<P: AsRef<Path>>(image_path: P, api_key: &Fa
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::remove_background_rembg::remove_background_rembg;
|
||||
use crate::requests::queue::remove_background_rembg::remove_background_rembg;
|
||||
use errors::AnyhowResult;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ pub async fn enqueue_kling_16_image_to_video<P: AsRef<Path>>(args: Kling16Args<'
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
|
||||
use crate::requests::queue::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
+1
-2
@@ -3,7 +3,6 @@ use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::model::enqueued_request::EnqueuedRequest;
|
||||
use fal::endpoints::fal_ai::minimax::image_to_video::image_to_video;
|
||||
use fal::endpoints::fal_ai::minimax::image_to_video::VideoOutput;
|
||||
use fal::endpoints::fal_ai::minimax::image_to_video::ImageToVideoRequest;
|
||||
use fal_client::file_to_base64_url::file_to_base64_url;
|
||||
use futures::StreamExt;
|
||||
@@ -41,10 +40,10 @@ pub async fn enqueue_minimax_01_image_to_video<P: AsRef<Path>>(args: Minimax01Ar
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::queue::video_gen::enqueue_minimax_01_image_to_video::{enqueue_minimax_01_image_to_video, Minimax01Args};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
use crate::requests::video_gen::enqueue_minimax_01_image_to_video::{enqueue_minimax_01_image_to_video, Minimax01Args};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
@@ -0,0 +1 @@
|
||||
pub mod remove_background_rembg_webhook;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::imageutils::rembg::{rembg, RemoveBackgroundInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct RemoveBackgroundRembgWebhookArgs<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub image_url: U,
|
||||
pub webhook_url: V,
|
||||
pub api_key: &'a FalApiKey
|
||||
}
|
||||
|
||||
pub async fn remove_background_rembg_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
args: RemoveBackgroundRembgWebhookArgs<'_, U, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let image_url = args.image_url.into_url()?;
|
||||
|
||||
let request = RemoveBackgroundInput {
|
||||
image_url: image_url.to_string(),
|
||||
crop_to_bbox: None,
|
||||
sync_mode: None
|
||||
};
|
||||
|
||||
let result = rembg(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::remove_background_rembg_webhook::remove_background_rembg_webhook;
|
||||
use crate::requests::webhook::image::remove_background_rembg_webhook::RemoveBackgroundRembgWebhookArgs;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // NB: Manually test, don't run in CI!
|
||||
async fn test_rembg_request() -> anyhow::Result<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let api_key = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&api_key);
|
||||
|
||||
let args = RemoveBackgroundRembgWebhookArgs {
|
||||
image_url: "https://cdn-2.fakeyou.com/media/b/2/p/b/0/b2pb09bcbxdqjzgbnsapvfs1t897wdnx/image_b2pb09bcbxdqjzgbnsapvfs1t897wdnx.png",
|
||||
webhook_url: "https://api.storyteller.ai/webhook",
|
||||
api_key: &api_key,
|
||||
};
|
||||
|
||||
let response = remove_background_rembg_webhook(args).await?;
|
||||
|
||||
println!("{:?}", response);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod video;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::hunyuan3d::v2::{v2, Hunyuan3DInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Hunyuan2Args<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub image_url: U,
|
||||
pub webhook_url: V,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Kling16Duration {
|
||||
Default,
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
pub async fn enqueue_hunyuan2_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
args: Hunyuan2Args<'_, U, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let image_url = args.image_url.as_str().to_string();
|
||||
|
||||
let request = Hunyuan3DInput {
|
||||
input_image_url: image_url,
|
||||
textured_mesh: Some(true),
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
octree_resolution: None,
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let result = v2(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod enqueue_hunyuan2_image_to_3d_webhook;
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::kling_video::v1_6::pro::image_to_video::{image_to_video, ProImageToVideoRequest};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Kling16Args<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub image_url: U,
|
||||
pub webhook_url: V,
|
||||
pub prompt: &'a str,
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub duration: Kling16Duration,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Kling16Duration {
|
||||
Default,
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
pub async fn enqueue_kling_16_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
args: Kling16Args<'_, U, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Kling16Duration::Default => None,
|
||||
Kling16Duration::FiveSeconds => Some("5".to_string()), // Gross...
|
||||
Kling16Duration::TenSeconds => Some("10".to_string()),
|
||||
};
|
||||
|
||||
let image_url = args.image_url.as_str().to_string();
|
||||
|
||||
let request = ProImageToVideoRequest {
|
||||
image_url,
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio: None,
|
||||
cfg_scale: None,
|
||||
duration,
|
||||
negative_prompt: None,
|
||||
tail_image_url: None,
|
||||
};
|
||||
|
||||
let result = image_to_video(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::{enqueue_kling_16_image_to_video_webhook, Kling16Args, Kling16Duration};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_kling16_video() -> AnyhowResult<()> {
|
||||
let image = test_file_path("test_data/image/juno.jpg")?;
|
||||
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Kling16Args {
|
||||
image_path: image,
|
||||
prompt: "a corgi looks out over the water",
|
||||
api_key: &api_key,
|
||||
duration: Kling16Duration::Default,
|
||||
};
|
||||
|
||||
let result = enqueue_kling_16_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1 @@
|
||||
pub mod enqueue_kling_16_image_to_video_webhook;
|
||||
+6
-4
@@ -6,9 +6,10 @@ use reqwest::Url;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use log::{debug, warn};
|
||||
use log::{debug, error, warn};
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::utils::classify_general_http_status_code_and_body::classify_general_http_status_code_and_body;
|
||||
|
||||
const SORA_STATUS_URL: &str = "https://sora.com/backend/video_gen";
|
||||
|
||||
@@ -241,9 +242,10 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
|
||||
debug!("response_body: {}", response_body);
|
||||
|
||||
if status != reqwest::StatusCode::OK {
|
||||
// TODO: Handle non-200 status codes, esp. JWT errors and credential errors
|
||||
return Err(SoraError::AnyhowError(anyhow::anyhow!("The physical status request failed; status = {:?}, message = {:?}", status, response_body)));
|
||||
if !status.is_success() {
|
||||
error!("The sora status request failed; status = {:?} ; response body = {}", status, response_body);
|
||||
let error = classify_general_http_status_code_and_body(status, &response_body).await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let maybe_response = serde_json::from_str::<VideoGenStatusResponse>(response_body);
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::web::Json;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SimpleGenericJsonSuccess {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
impl SimpleGenericJsonSuccess {
|
||||
pub fn wrapped(success: bool) -> Json<Self> {
|
||||
Json(SimpleGenericJsonSuccess { success })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple_json_success() -> HttpResponse {
|
||||
let response = SimpleGenericJsonSuccess {
|
||||
success: true,
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
|
||||
use log::warn;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::helpers::boolean_converters::i8_to_bool;
|
||||
use crate::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, PolymorphicInferenceArgs};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FalJobDetails {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
pub status: JobStatusPlus,
|
||||
|
||||
pub external_third_party: InferenceJobExternalThirdParty,
|
||||
pub external_third_party_id: String,
|
||||
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Returns Ok(None) when the record cannot be found.
|
||||
pub async fn get_inference_job_by_fal_id(fal_id: &str, mysql_pool: &MySqlPool)
|
||||
-> AnyhowResult<Option<FalJobDetails>>
|
||||
{
|
||||
let mut connection = mysql_pool.acquire().await?;
|
||||
get_inference_job_by_fal_id_from_connection(fal_id, &mut connection).await
|
||||
}
|
||||
|
||||
|
||||
/// Returns Ok(None) when the record cannot be found.
|
||||
pub async fn get_inference_job_by_fal_id_from_connection(fal_id: &str, mysql_connection: &mut PoolConnection<MySql>)
|
||||
-> AnyhowResult<Option<FalJobDetails>>
|
||||
{
|
||||
// NB(bt): jobs.uuid_idempotency_token is the current way to reconstruct the hash of the
|
||||
// TTS result since we don't store a bucket hash on the table. This is an ugly hack :(
|
||||
// TODO(bt,2023-10-12): ^^^ Is this comment still accurate? I don't see that field referenced below.
|
||||
|
||||
let maybe_status = sqlx::query_as!(
|
||||
RawJobRecord,
|
||||
r#"
|
||||
SELECT
|
||||
jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,
|
||||
|
||||
jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,
|
||||
|
||||
jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,
|
||||
jobs.maybe_external_third_party_id as `external_third_party_id`,
|
||||
|
||||
jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,
|
||||
jobs.creator_ip_address,
|
||||
|
||||
jobs.created_at,
|
||||
jobs.updated_at
|
||||
|
||||
FROM generic_inference_jobs as jobs
|
||||
|
||||
WHERE jobs.maybe_external_third_party = ?
|
||||
AND jobs.maybe_external_third_party_id = ?
|
||||
"#,
|
||||
InferenceJobExternalThirdParty::Fal.to_str(),
|
||||
fal_id
|
||||
)
|
||||
.fetch_one(&mut **mysql_connection)
|
||||
.await;
|
||||
|
||||
let record = match maybe_status {
|
||||
Ok(record) => record,
|
||||
Err(err) => match err {
|
||||
sqlx::Error::RowNotFound => return Ok(None),
|
||||
_ => {
|
||||
warn!("error querying job record: {:?}", err);
|
||||
return Err(anyhow!("error querying job record: {:?}", err));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let record = raw_record_to_public_result(record)?;
|
||||
|
||||
Ok(Some(record))
|
||||
}
|
||||
|
||||
fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<FalJobDetails> {
|
||||
Ok(FalJobDetails {
|
||||
job_token: record.job_token,
|
||||
status: record.status,
|
||||
external_third_party: record.external_third_party.ok_or_else(|| anyhow!("missing external_third_party"))?,
|
||||
external_third_party_id: record.external_third_party_id.ok_or_else(|| anyhow!("missing external_third_party_id"))?,
|
||||
maybe_creator_user_token: record.maybe_creator_user_token,
|
||||
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
|
||||
creator_ip_address: record.creator_ip_address,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RawJobRecord {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
pub status: JobStatusPlus,
|
||||
|
||||
// NB: Nullable, but required to be filled for this query
|
||||
pub external_third_party: Option<InferenceJobExternalThirdParty>,
|
||||
|
||||
// NB: Nullable, but required to be filled for this query
|
||||
pub external_third_party_id: Option<String>,
|
||||
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::common::visibility::Visibility;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::errors::database_query_error::DatabaseQueryError;
|
||||
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
|
||||
use crate::queries::tts::tts_inference_jobs::kill_tts_inference_jobs::JobStatus;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalCategory {
|
||||
ImageGeneration,
|
||||
VideoGeneration,
|
||||
BackgroundRemoval,
|
||||
ObjectGeneration,
|
||||
}
|
||||
|
||||
pub struct InsertGenericInferenceForFalArgs<'a> {
|
||||
pub uuid_idempotency_token: &'a str,
|
||||
|
||||
/// The external primary key identifier for the job.
|
||||
pub maybe_external_third_party_id: &'a str,
|
||||
|
||||
pub fal_category: FalCategory,
|
||||
|
||||
pub maybe_inference_args: Option<GenericInferenceArgs>,
|
||||
|
||||
pub maybe_creator_user_token: Option<&'a UserToken>,
|
||||
pub maybe_avt_token: Option<&'a AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: &'a str,
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
pub async fn insert_generic_inference_job_for_fal_queue(args: InsertGenericInferenceForFalArgs<'_>)
|
||||
-> Result<InferenceJobToken, DatabaseQueryError>
|
||||
{
|
||||
let job_token = InferenceJobToken::generate();
|
||||
|
||||
let serialized_args_payload = serde_json::ser::to_string(&args.maybe_inference_args)
|
||||
.map_err(|_e| anyhow!("could not encode inference args"))?;
|
||||
|
||||
|
||||
const JOB_TYPE : InferenceJobType = InferenceJobType::FalQueue;
|
||||
|
||||
let (inference_category, product_category) =
|
||||
match args.fal_category {
|
||||
FalCategory::ImageGeneration => (
|
||||
InferenceCategory::ImageGeneration,
|
||||
InferenceJobProductCategory::FalImage
|
||||
),
|
||||
FalCategory::VideoGeneration => (
|
||||
InferenceCategory::VideoGeneration,
|
||||
InferenceJobProductCategory::FalVideo
|
||||
),
|
||||
FalCategory::BackgroundRemoval => (
|
||||
InferenceCategory::BackgroundRemoval,
|
||||
InferenceJobProductCategory::FalBgRemoval
|
||||
),
|
||||
FalCategory::ObjectGeneration => (
|
||||
InferenceCategory::ObjectGeneration,
|
||||
InferenceJobProductCategory::FalObject,
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
let maybe_external_third_party = InferenceJobExternalThirdParty::Fal;
|
||||
|
||||
let maybe_external_third_party_id = "";
|
||||
const STATUS : JobStatusPlus = JobStatusPlus::Pending;
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO generic_inference_jobs
|
||||
SET
|
||||
token = ?,
|
||||
uuid_idempotency_token = ?,
|
||||
|
||||
job_type = ?,
|
||||
|
||||
maybe_external_third_party = ?,
|
||||
maybe_external_third_party_id = ?,
|
||||
|
||||
product_category = ?,
|
||||
inference_category = ?,
|
||||
|
||||
maybe_model_type = NULL,
|
||||
maybe_model_token = NULL,
|
||||
|
||||
maybe_input_source_token = NULL,
|
||||
maybe_input_source_token_type = NULL,
|
||||
|
||||
maybe_download_url = NULL,
|
||||
maybe_cover_image_media_file_token = NULL,
|
||||
|
||||
maybe_raw_inference_text = NULL,
|
||||
|
||||
maybe_inference_args = ?,
|
||||
|
||||
maybe_creator_user_token = ?,
|
||||
maybe_creator_anonymous_visitor_token = ?,
|
||||
creator_ip_address = ?,
|
||||
creator_set_visibility = ?,
|
||||
|
||||
priority_level = 0,
|
||||
is_keepalive_required = FALSE,
|
||||
max_duration_seconds = 0,
|
||||
|
||||
is_debug_request = FALSE,
|
||||
maybe_routing_tag = NULL,
|
||||
|
||||
status = ?
|
||||
"#,
|
||||
job_token.as_str(),
|
||||
args.uuid_idempotency_token,
|
||||
|
||||
JOB_TYPE.to_str(),
|
||||
|
||||
maybe_external_third_party.to_str(),
|
||||
args.maybe_external_third_party_id,
|
||||
|
||||
product_category.to_str(),
|
||||
inference_category.to_str(),
|
||||
|
||||
serialized_args_payload,
|
||||
|
||||
args.maybe_creator_user_token.map(|t| t.to_string()),
|
||||
args.maybe_avt_token.map(|t| t.to_string()),
|
||||
args.creator_ip_address,
|
||||
args.creator_set_visibility.to_str(),
|
||||
|
||||
STATUS.to_str(),
|
||||
);
|
||||
|
||||
let query_result = query.execute(args.mysql_pool)
|
||||
.await;
|
||||
|
||||
let record_id = match query_result {
|
||||
Err(err) => return Err(DatabaseQueryError::from(err)),
|
||||
Ok(res) => res.last_insert_id(),
|
||||
};
|
||||
|
||||
info!("Insert generic inference job for FAL queue: {job_token} with record ID {record_id}");
|
||||
|
||||
Ok(job_token)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
// TODO: Maybe in the future we set a batch_token instead of media token
|
||||
|
||||
pub async fn mark_fal_generic_inference_job_successfully_done(
|
||||
pool: &MySqlPool,
|
||||
job_token: &InferenceJobToken,
|
||||
media_file_token: MediaFileToken,
|
||||
) -> AnyhowResult<()>
|
||||
{
|
||||
const STATUS : JobStatusPlus = JobStatusPlus::CompleteSuccess;
|
||||
const RESULT_TYPE : InferenceResultType = InferenceResultType::MediaFile;
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE generic_inference_jobs
|
||||
SET
|
||||
status = ?,
|
||||
on_success_result_entity_type = ?,
|
||||
on_success_result_entity_token = ?,
|
||||
failure_reason = NULL,
|
||||
internal_debugging_failure_reason = NULL,
|
||||
success_execution_millis = NULL,
|
||||
success_inference_execution_millis = NULL,
|
||||
retry_at = NULL,
|
||||
successfully_completed_at = NOW()
|
||||
WHERE token = ?
|
||||
"#,
|
||||
STATUS.to_str(),
|
||||
RESULT_TYPE.to_str(),
|
||||
media_file_token,
|
||||
job_token,
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match query_result {
|
||||
Err(err) => Err(anyhow!("error with query: {:?}", err)),
|
||||
Ok(_r) => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod get_inference_job_by_fal_id;
|
||||
pub mod insert_generic_inference_job_for_fal_queue;
|
||||
pub mod mark_fal_generic_inference_job_successfully_done;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod fal;
|
||||
pub mod job;
|
||||
pub mod web;
|
||||
|
||||
+6
@@ -174,6 +174,9 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::VoiceConversion => record.maybe_voice_conversion_model_title.as_deref(),
|
||||
InferenceCategory::VideoFilter => Some("Video Filter"),
|
||||
InferenceCategory::ImageGeneration => Some("Image Generation"),
|
||||
InferenceCategory::VideoGeneration => Some("Video Generation"),
|
||||
InferenceCategory::ObjectGeneration => Some("Object Generation"),
|
||||
InferenceCategory::BackgroundRemoval => Some("Background Removal"),
|
||||
InferenceCategory::Mocap => Some("Mocap"),
|
||||
InferenceCategory::Workflow => Some("Workflow"),
|
||||
InferenceCategory::FormatConversion => Some("format conversion"),
|
||||
@@ -194,6 +197,9 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::VoiceConversion => (true, record.maybe_voice_conversion_public_bucket_hash.as_deref()),
|
||||
InferenceCategory::VideoFilter => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ImageGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::VideoGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ObjectGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::BackgroundRemoval => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::Mocap => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::Workflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::FormatConversion => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
|
||||
+6
@@ -150,6 +150,9 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
|
||||
InferenceCategory::VoiceConversion => record.maybe_voice_conversion_model_title.as_deref(),
|
||||
InferenceCategory::VideoFilter => Some("Video Filter"),
|
||||
InferenceCategory::ImageGeneration => Some("Image Generation"),
|
||||
InferenceCategory::VideoGeneration => Some("Video Generation"),
|
||||
InferenceCategory::ObjectGeneration => Some("Object Generation"),
|
||||
InferenceCategory::BackgroundRemoval => Some("Background Removal"),
|
||||
InferenceCategory::Mocap => Some("Mocap"),
|
||||
InferenceCategory::Workflow => Some("Workflow"),
|
||||
InferenceCategory::FormatConversion => Some("format conversion"),
|
||||
@@ -170,6 +173,9 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
|
||||
InferenceCategory::VoiceConversion => (true, record.maybe_voice_conversion_public_bucket_hash.as_deref()),
|
||||
InferenceCategory::VideoFilter => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ImageGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::VideoGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ObjectGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::BackgroundRemoval => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::Mocap => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::Workflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::FormatConversion => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
|
||||
+6
@@ -241,6 +241,9 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::VoiceConversion => record.maybe_voice_conversion_model_title.as_deref(),
|
||||
InferenceCategory::VideoFilter => Some("Video Filter"),
|
||||
InferenceCategory::ImageGeneration => Some("Image Generation"),
|
||||
InferenceCategory::VideoGeneration => Some("Video Generation"),
|
||||
InferenceCategory::ObjectGeneration => Some("Object Generation"),
|
||||
InferenceCategory::BackgroundRemoval => Some("Background Removal"),
|
||||
InferenceCategory::Mocap => Some("Mocap"),
|
||||
InferenceCategory::Workflow => Some("Workflow"),
|
||||
InferenceCategory::FormatConversion => Some("format conversion"),
|
||||
@@ -260,6 +263,9 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::VoiceConversion => (true, record.maybe_voice_conversion_public_bucket_hash.as_deref()),
|
||||
InferenceCategory::VideoFilter => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ImageGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::VideoGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ObjectGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::BackgroundRemoval => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::Mocap => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::Workflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::FormatConversion => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
|
||||
+19
-4
@@ -44,7 +44,7 @@ pub struct MediaFileInsertBuilder {
|
||||
// Media info
|
||||
maybe_mime_type: Option<String>,
|
||||
file_size_bytes: u64, // NB: Non-nullable field
|
||||
// maybe_duration_millis: Option<u64>,
|
||||
maybe_duration_millis: Option<u64>,
|
||||
// maybe_audio_encoding: Option<&'a str>,
|
||||
// maybe_video_encoding: Option<&'a str>,
|
||||
maybe_frame_width: Option<u32>,
|
||||
@@ -90,6 +90,7 @@ impl MediaFileInsertBuilder {
|
||||
origin_product_category: MediaFileOriginProductCategory::Unknown,
|
||||
maybe_mime_type: None,
|
||||
file_size_bytes: 0,
|
||||
maybe_duration_millis: None,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
checksum_sha2: None,
|
||||
@@ -173,19 +174,33 @@ impl MediaFileInsertBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: maybe_duration_millis
|
||||
pub fn maybe_duration_millis(mut self, maybe_duration_millis: Option<u64>) -> Self {
|
||||
self.maybe_duration_millis = maybe_duration_millis;
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: maybe_audio_encoding
|
||||
// TODO: maybe_video_encoding
|
||||
|
||||
|
||||
pub fn frame_width(mut self, frame_width: u32) -> Self {
|
||||
self.maybe_frame_width = Some(frame_width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn maybe_frame_width(mut self, maybe_frame_width: Option<u32>) -> Self {
|
||||
self.maybe_frame_width = maybe_frame_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn frame_height(mut self, frame_height: u32) -> Self {
|
||||
self.maybe_frame_height = Some(frame_height);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn maybe_frame_height(mut self, maybe_frame_height: Option<u32>) -> Self {
|
||||
self.maybe_frame_height = maybe_frame_height;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn checksum_sha2(mut self, checksum_sha2: &str) -> Self {
|
||||
self.checksum_sha2 = Some(checksum_sha2.to_string());
|
||||
@@ -238,7 +253,7 @@ impl MediaFileInsertBuilder {
|
||||
maybe_origin_filename: None, // TODO
|
||||
maybe_mime_type: self.maybe_mime_type.as_deref(),
|
||||
file_size_bytes: self.file_size_bytes,
|
||||
maybe_duration_millis: None, // TODO
|
||||
maybe_duration_millis: self.maybe_duration_millis,
|
||||
maybe_audio_encoding: None, // TODO
|
||||
maybe_video_encoding: None, // TODO
|
||||
maybe_frame_width: self.maybe_frame_width,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "artcraft_api_defs"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "artcraft_api_defs"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Hackari workspace (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
enums = { workspace = true }
|
||||
tokens = { workspace = true }
|
||||
|
||||
# Serialization
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
utoipa = { version = "4", features = ["chrono", "actix_extras"] }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -0,0 +1 @@
|
||||
pub mod remove_image_background;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct RemoveImageBackgroundRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Source of the file to remove the background from.
|
||||
/// It must be an image.
|
||||
pub media_file_token: Option<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct RemoveImageBackgroundResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod video;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
const GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2_image_to_3d";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateHunyuan2ImageTo3dRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Source of the file to convert to 3D
|
||||
/// It must be an image.
|
||||
pub media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateHunyuan2ImageTo3dResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate_hunyuan_2_image_to_3d;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
const GENERATE_KLING_1_6_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_16_image_to_video";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateKling16ImageToVideoRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Source of the image file to convert to video.
|
||||
/// It must be an image.
|
||||
pub media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateKling16ImageToVideoResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate_kling_1_6_image_to_video;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate;
|
||||
+35
-17
@@ -35,10 +35,22 @@ pub enum InferenceCategory {
|
||||
#[serde(rename = "voice_conversion")]
|
||||
VoiceConversion,
|
||||
|
||||
/// Image generation (eg. Stable Diffusion 1.5)
|
||||
/// Image generation (eg. Stable Diffusion 1.5), FAL-powered image generation, etc.
|
||||
#[serde(rename = "image_generation")]
|
||||
ImageGeneration,
|
||||
|
||||
/// FAL-powered video generation
|
||||
#[serde(rename = "video_generation")]
|
||||
VideoGeneration,
|
||||
|
||||
/// FAL-powered 3D object generation
|
||||
#[serde(rename = "object_generation")]
|
||||
ObjectGeneration,
|
||||
|
||||
/// FAL-powered image background removal
|
||||
#[serde(rename = "background_removal")]
|
||||
BackgroundRemoval,
|
||||
|
||||
/// Turn video into animation data with mocap processing (eg. Mocapnet).
|
||||
#[serde(rename = "mocap")]
|
||||
Mocap,
|
||||
@@ -89,6 +101,9 @@ impl InferenceCategory {
|
||||
Self::TextToSpeech => "text_to_speech",
|
||||
Self::VoiceConversion => "voice_conversion",
|
||||
Self::ImageGeneration => "image_generation",
|
||||
Self::VideoGeneration => "video_generation",
|
||||
Self::ObjectGeneration => "object_generation",
|
||||
Self::BackgroundRemoval => "background_removal",
|
||||
Self::Mocap => "mocap",
|
||||
Self::Workflow => "workflow",
|
||||
Self::F5TTS => "f5_tts",
|
||||
@@ -107,6 +122,9 @@ impl InferenceCategory {
|
||||
"text_to_speech" => Ok(Self::TextToSpeech),
|
||||
"voice_conversion" => Ok(Self::VoiceConversion),
|
||||
"image_generation" => Ok(Self::ImageGeneration),
|
||||
"video_generation" => Ok(Self::VideoGeneration),
|
||||
"object_generation" => Ok(Self::ObjectGeneration),
|
||||
"background_removal" => Ok(Self::BackgroundRemoval),
|
||||
"f5_tts" => Ok(Self::F5TTS),
|
||||
"mocap" => Ok(Self::Mocap),
|
||||
"workflow" => Ok(Self::Workflow),
|
||||
@@ -128,6 +146,9 @@ impl InferenceCategory {
|
||||
Self::TextToSpeech,
|
||||
Self::VoiceConversion,
|
||||
Self::ImageGeneration,
|
||||
Self::ObjectGeneration,
|
||||
Self::VideoGeneration,
|
||||
Self::BackgroundRemoval,
|
||||
Self::Mocap,
|
||||
Self::F5TTS,
|
||||
Self::SeedVc,
|
||||
@@ -155,6 +176,9 @@ mod tests {
|
||||
assert_serialization(InferenceCategory::TextToSpeech, "text_to_speech");
|
||||
assert_serialization(InferenceCategory::VoiceConversion, "voice_conversion");
|
||||
assert_serialization(InferenceCategory::ImageGeneration, "image_generation");
|
||||
assert_serialization(InferenceCategory::VideoGeneration, "video_generation");
|
||||
assert_serialization(InferenceCategory::ObjectGeneration, "object_generation");
|
||||
assert_serialization(InferenceCategory::BackgroundRemoval, "background_removal");
|
||||
assert_serialization(InferenceCategory::Mocap, "mocap");
|
||||
assert_serialization(InferenceCategory::F5TTS, "f5_tts");
|
||||
assert_serialization(InferenceCategory::SeedVc, "seed_vc");
|
||||
@@ -172,6 +196,9 @@ mod tests {
|
||||
assert_eq!(InferenceCategory::TextToSpeech.to_str(), "text_to_speech");
|
||||
assert_eq!(InferenceCategory::VoiceConversion.to_str(), "voice_conversion");
|
||||
assert_eq!(InferenceCategory::ImageGeneration.to_str(), "image_generation");
|
||||
assert_eq!(InferenceCategory::VideoGeneration.to_str(), "video_generation");
|
||||
assert_eq!(InferenceCategory::ObjectGeneration.to_str(), "object_generation");
|
||||
assert_eq!(InferenceCategory::BackgroundRemoval.to_str(), "background_removal");
|
||||
assert_eq!(InferenceCategory::F5TTS.to_str(), "f5_tts");
|
||||
assert_eq!(InferenceCategory::SeedVc.to_str(), "seed_vc");
|
||||
assert_eq!(InferenceCategory::Mocap.to_str(), "mocap");
|
||||
@@ -189,6 +216,9 @@ mod tests {
|
||||
assert_eq!(InferenceCategory::from_str("text_to_speech").unwrap(), InferenceCategory::TextToSpeech);
|
||||
assert_eq!(InferenceCategory::from_str("voice_conversion").unwrap(), InferenceCategory::VoiceConversion);
|
||||
assert_eq!(InferenceCategory::from_str("image_generation").unwrap(), InferenceCategory::ImageGeneration);
|
||||
assert_eq!(InferenceCategory::from_str("video_generation").unwrap(), InferenceCategory::VideoGeneration);
|
||||
assert_eq!(InferenceCategory::from_str("object_generation").unwrap(), InferenceCategory::ObjectGeneration);
|
||||
assert_eq!(InferenceCategory::from_str("background_removal").unwrap(), InferenceCategory::BackgroundRemoval);
|
||||
assert_eq!(InferenceCategory::from_str("f5_tts").unwrap(), InferenceCategory::F5TTS);
|
||||
assert_eq!(InferenceCategory::from_str("seed_vc").unwrap(), InferenceCategory::SeedVc);
|
||||
assert_eq!(InferenceCategory::from_str("mocap").unwrap(), InferenceCategory::Mocap);
|
||||
@@ -202,22 +232,10 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
let mut variants = InferenceCategory::all_variants();
|
||||
assert_eq!(variants.len(), 12);
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::DeprecatedField));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::LipsyncAnimation));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::TextToSpeech));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::VoiceConversion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::ImageGeneration));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::F5TTS));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::Mocap));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::SeedVc));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::Workflow));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::FormatConversion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::LivePortrait));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::VideoFilter));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceCategory::ConvertBvhToWorkflow));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
const EXPECTED_COUNT : usize = 16;
|
||||
|
||||
assert_eq!(InferenceCategory::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceCategory::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
// Generated check
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
|
||||
/// Used in the `generic_inference_jobs` table in `VARCHAR(16)` field `maybe_external_third_party`.
|
||||
///
|
||||
/// YOU CAN ADD NEW VALUES, BUT DO NOT CHANGE EXISTING VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InferenceJobExternalThirdParty {
|
||||
/// Fal jobs
|
||||
#[serde(rename = "fal")]
|
||||
#[default]
|
||||
Fal,
|
||||
}
|
||||
|
||||
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
|
||||
impl_enum_display_and_debug_using_to_str!(InferenceJobExternalThirdParty);
|
||||
impl_mysql_enum_coders!(InferenceJobExternalThirdParty);
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl InferenceJobExternalThirdParty {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Fal => "fal",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"fal" => Ok(Self::Fal),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::Fal,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(InferenceJobExternalThirdParty::Fal, "fal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(InferenceJobExternalThirdParty::Fal.to_str(), "fal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("fal").unwrap(), InferenceJobExternalThirdParty::Fal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 1;
|
||||
|
||||
assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobExternalThirdParty::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
// Generated check
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), InferenceJobExternalThirdParty::iter().len());
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), InferenceJobExternalThirdParty::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in InferenceJobExternalThirdParty::all_variants() {
|
||||
assert_eq!(variant, InferenceJobExternalThirdParty::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, InferenceJobExternalThirdParty::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, InferenceJobExternalThirdParty::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 32;
|
||||
for variant in InferenceJobExternalThirdParty::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-25
@@ -21,6 +21,14 @@ pub enum InferenceJobProductCategory {
|
||||
/// Download: GptSoVits
|
||||
#[default]
|
||||
DownloadGptSoVits,
|
||||
|
||||
// =============== FAL ===============
|
||||
|
||||
FalImage,
|
||||
FalVideo,
|
||||
/// Fal: 3D Object Generation
|
||||
FalObject,
|
||||
FalBgRemoval,
|
||||
|
||||
// =============== TEXT TO SPEECH ===============
|
||||
|
||||
@@ -113,6 +121,10 @@ impl InferenceJobProductCategory {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::DownloadGptSoVits => "download_gpt_so_vits",
|
||||
Self::FalImage => "fal_image",
|
||||
Self::FalVideo => "fal_video",
|
||||
Self::FalObject => "fal_object",
|
||||
Self::FalBgRemoval => "fal_bg_removal",
|
||||
Self::TtsGptSoVits => "tts_gpt_so_vits",
|
||||
Self::TtsStyleTts2 => "tts_style_tts2",
|
||||
Self::TtsTacotron2 => "tts_tacotron2",
|
||||
@@ -141,6 +153,10 @@ impl InferenceJobProductCategory {
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"download_gpt_so_vits" => Ok(Self::DownloadGptSoVits),
|
||||
"fal_image" => Ok(Self::FalImage),
|
||||
"fal_video" => Ok(Self::FalVideo),
|
||||
"fal_object" => Ok(Self::FalObject),
|
||||
"fal_bg_removal" => Ok(Self::FalBgRemoval),
|
||||
"tts_gpt_so_vits" => Ok(Self::TtsGptSoVits),
|
||||
"tts_style_tts2" => Ok(Self::TtsStyleTts2),
|
||||
"tts_tacotron2" => Ok(Self::TtsTacotron2),
|
||||
@@ -172,6 +188,10 @@ impl InferenceJobProductCategory {
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::DownloadGptSoVits,
|
||||
Self::FalImage,
|
||||
Self::FalVideo,
|
||||
Self::FalObject,
|
||||
Self::FalBgRemoval,
|
||||
Self::TtsGptSoVits,
|
||||
Self::TtsStyleTts2,
|
||||
Self::TtsTacotron2,
|
||||
@@ -209,6 +229,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(InferenceJobProductCategory::DownloadGptSoVits, "download_gpt_so_vits");
|
||||
assert_serialization(InferenceJobProductCategory::FalImage, "fal_image");
|
||||
assert_serialization(InferenceJobProductCategory::FalVideo, "fal_video");
|
||||
assert_serialization(InferenceJobProductCategory::FalObject, "fal_object");
|
||||
assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal");
|
||||
assert_serialization(InferenceJobProductCategory::TtsGptSoVits, "tts_gpt_so_vits");
|
||||
assert_serialization(InferenceJobProductCategory::TtsStyleTts2, "tts_style_tts2");
|
||||
assert_serialization(InferenceJobProductCategory::TtsTacotron2, "tts_tacotron2");
|
||||
@@ -236,6 +260,10 @@ mod tests {
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(InferenceJobProductCategory::DownloadGptSoVits.to_str(), "download_gpt_so_vits");
|
||||
assert_eq!(InferenceJobProductCategory::FalImage.to_str(), "fal_image");
|
||||
assert_eq!(InferenceJobProductCategory::FalVideo.to_str(), "fal_video");
|
||||
assert_eq!(InferenceJobProductCategory::FalObject.to_str(), "fal_object");
|
||||
assert_eq!(InferenceJobProductCategory::FalBgRemoval.to_str(), "fal_bg_removal");
|
||||
assert_eq!(InferenceJobProductCategory::TtsGptSoVits.to_str(), "tts_gpt_so_vits");
|
||||
assert_eq!(InferenceJobProductCategory::TtsStyleTts2.to_str(), "tts_style_tts2");
|
||||
assert_eq!(InferenceJobProductCategory::TtsTacotron2.to_str(), "tts_tacotron2");
|
||||
@@ -262,6 +290,10 @@ mod tests {
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(InferenceJobProductCategory::from_str("download_gpt_so_vits").unwrap(), InferenceJobProductCategory::DownloadGptSoVits);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_image").unwrap(), InferenceJobProductCategory::FalImage);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_video").unwrap(), InferenceJobProductCategory::FalVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_object").unwrap(), InferenceJobProductCategory::FalObject);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_bg_removal").unwrap(), InferenceJobProductCategory::FalBgRemoval);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("tts_gpt_so_vits").unwrap(), InferenceJobProductCategory::TtsGptSoVits);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("tts_style_tts2").unwrap(), InferenceJobProductCategory::TtsStyleTts2);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("tts_tacotron2").unwrap(), InferenceJobProductCategory::TtsTacotron2);
|
||||
@@ -288,32 +320,11 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
let mut variants = InferenceJobProductCategory::all_variants();
|
||||
assert_eq!(variants.len(), 21);
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::DownloadGptSoVits));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::TtsGptSoVits));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::TtsStyleTts2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::TtsTacotron2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VcRvc2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VcSeedVc));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VcSvc));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidLipsyncFaceFusion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidLipsyncSadTalker));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidLivePortrait));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidLivePortraitWebcam));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidStudio));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidStudioGen2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidStyleTransfer));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::LipsyncFaceFusion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::LipsyncSadTalker));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::LivePortrait));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::LivePortraitWebcam));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::StableDiffusion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::Studio));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidFaceFusion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::Vst));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
const EXPECTED_COUNT : usize = 27;
|
||||
|
||||
assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
// Generated check
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(InferenceJobProductCategory::all_variants().len(), InferenceJobProductCategory::iter().len());
|
||||
|
||||
+14
-23
@@ -15,6 +15,10 @@ use strum::EnumIter;
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InferenceJobType {
|
||||
/// Fal jobs do not run on our job runners.
|
||||
/// Instead, we receive web hook updates from Fal.
|
||||
FalQueue,
|
||||
|
||||
/// Storyteller Studio and Video Style Transfer Jobs (which we may want to split).
|
||||
/// These run in Comfy.
|
||||
/// TODO(bt,2024-07-15): We may segregate these two job types in the future
|
||||
@@ -105,6 +109,7 @@ impl_mysql_enum_coders!(InferenceJobType);
|
||||
impl InferenceJobType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::FalQueue => "fal_queue",
|
||||
Self::VideoRender => "video_render",
|
||||
Self::LivePortrait => "live_portrait",
|
||||
Self::FaceFusion => "face_fusion",
|
||||
@@ -130,6 +135,7 @@ impl InferenceJobType {
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"fal_queue" => Ok(Self::FalQueue),
|
||||
"video_render" => Ok(Self::VideoRender),
|
||||
"live_portrait" => Ok(Self::LivePortrait),
|
||||
"face_fusion" => Ok(Self::FaceFusion),
|
||||
@@ -158,6 +164,7 @@ impl InferenceJobType {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::FalQueue,
|
||||
Self::VideoRender,
|
||||
Self::LivePortrait,
|
||||
Self::FaceFusion,
|
||||
@@ -197,6 +204,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(InferenceJobType::FalQueue, "fal_queue");
|
||||
assert_serialization(InferenceJobType::VideoRender, "video_render");
|
||||
assert_serialization(InferenceJobType::LivePortrait, "live_portrait");
|
||||
assert_serialization(InferenceJobType::FaceFusion, "face_fusion");
|
||||
@@ -221,6 +229,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(InferenceJobType::FalQueue.to_str(), "fal_queue");
|
||||
assert_eq!(InferenceJobType::VideoRender.to_str(), "video_render");
|
||||
assert_eq!(InferenceJobType::LivePortrait.to_str(), "live_portrait");
|
||||
assert_eq!(InferenceJobType::FaceFusion.to_str(), "face_fusion");
|
||||
@@ -245,6 +254,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(InferenceJobType::from_str("fal_queue").unwrap(), InferenceJobType::FalQueue);
|
||||
assert_eq!(InferenceJobType::from_str("video_render").unwrap(), InferenceJobType::VideoRender);
|
||||
assert_eq!(InferenceJobType::from_str("live_portrait").unwrap(), InferenceJobType::LivePortrait);
|
||||
assert_eq!(InferenceJobType::from_str("face_fusion").unwrap(), InferenceJobType::FaceFusion);
|
||||
@@ -270,29 +280,10 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
let mut variants = InferenceJobType::all_variants();
|
||||
assert_eq!(variants.len(), 17);
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::VideoRender));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::LivePortrait));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::FaceFusion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::F5TTS));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::GptSovits));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::ComfyUi));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::StudioGen2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::ImageGenApi));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::ConvertFbxToGltf));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::MocapNet));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::RvcV2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::SadTalker));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::SeedVc));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::SoVitsSvc));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::StableDiffusion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::StyleTTS2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::Tacotron2));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::Unknown));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::BevyToWorkflow));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceJobType::RerenderAVideo));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
const EXPECTED_COUNT : usize = 21;
|
||||
|
||||
assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
// Generated check
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod frontend_failure_category;
|
||||
pub mod inference_category;
|
||||
pub mod inference_input_source_token_type;
|
||||
pub mod inference_job_external_third_party;
|
||||
pub mod inference_job_product_category;
|
||||
pub mod inference_job_type;
|
||||
pub mod inference_model_type;
|
||||
|
||||
@@ -91,6 +91,36 @@ impl_mysql_from_row!(MediaFileType);
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl MediaFileType {
|
||||
|
||||
/// Jpeg and Png are the most widely supported static image formats for AI inference.
|
||||
/// Webp, Webm, and Gif aren't as widely supported, so we don't include them here.
|
||||
pub fn is_jpg_or_png(&self) -> bool {
|
||||
matches!(self, Self::Jpg | Self::Png)
|
||||
}
|
||||
|
||||
/// A little bit less strict than `is_jpg_or_png`, this includes the legacy `Image` type.
|
||||
pub fn is_jpg_or_png_or_legacy_image(&self) -> bool {
|
||||
matches!(self, Self::Jpg | Self::Png | Self::Image)
|
||||
}
|
||||
|
||||
/// Returns the `MediaFileType` if the mime type matches one of the known types.
|
||||
/// This is not exhaustive.
|
||||
/// (NB: In general we shouldn't keep much application logic in the `enums` crate,
|
||||
/// but this is a good case for it.)
|
||||
pub fn try_from_mime_type(mime_type: &str) -> Option<Self> {
|
||||
match mime_type {
|
||||
"image/jpeg" => Some(Self::Jpg),
|
||||
"image/png" => Some(Self::Png),
|
||||
"video/mp4" => Some(Self::Mp4),
|
||||
"model/gltf-binary" => Some(Self::Glb),
|
||||
//"audio/wav" => Some(Self::Wav), // NB: Not sure if this is correct
|
||||
//"audio/mpeg" => Some(Self::Mp3), // NB: Not sure if this is correct
|
||||
//"image/gif" => Some(Self::Gif), // NB: Not sure if this is correct
|
||||
//"model/gltf+json" => Some(Self::Gltf), // NB: Not sure if this is correct
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Audio => "audio",
|
||||
@@ -171,6 +201,29 @@ impl MediaFileType {
|
||||
mod tests {
|
||||
use crate::by_table::media_files::media_file_type::MediaFileType;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod utility {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_jpg_or_png() {
|
||||
// True
|
||||
assert!(MediaFileType::Jpg.is_jpg_or_png());
|
||||
assert!(MediaFileType::Png.is_jpg_or_png());
|
||||
|
||||
// Assert these image types are false
|
||||
assert!(!MediaFileType::Gif.is_jpg_or_png());
|
||||
assert!(!MediaFileType::Image.is_jpg_or_png());
|
||||
|
||||
// Everything else is false
|
||||
for variant in MediaFileType::all_variants() {
|
||||
if matches!(variant, MediaFileType::Jpg | MediaFileType::Png) {
|
||||
continue;
|
||||
}
|
||||
assert!(!variant.is_jpg_or_png(), "Expected {:?} to not be jpg or png", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod serde {
|
||||
use super::*;
|
||||
|
||||
+7
@@ -32,8 +32,13 @@ pub enum PublicInferenceJobType {
|
||||
|
||||
#[serde(rename = "f5_tts")]
|
||||
F5TTS,
|
||||
|
||||
// ======= Everything else is the same =======
|
||||
|
||||
/// No need to hide this.
|
||||
#[serde(rename = "fal_queue")]
|
||||
FalQueue,
|
||||
|
||||
/// Jobs that run ComfyUI workflows
|
||||
/// This is actually just for Video Style Transfer and Storyteller Studio.
|
||||
#[deprecated(note = "Use VideoRender instead.")]
|
||||
@@ -106,6 +111,7 @@ impl PublicInferenceJobType {
|
||||
InferenceJobType::LivePortrait => Self::ActingFace,
|
||||
InferenceJobType::FaceFusion => Self::Lipsync,
|
||||
// Conserved variants
|
||||
InferenceJobType::FalQueue => Self::FalQueue,
|
||||
InferenceJobType::VideoRender => Self::VideoRender,
|
||||
InferenceJobType::GptSovits => Self::GptSovits,
|
||||
InferenceJobType::F5TTS => Self::F5TTS,
|
||||
@@ -133,6 +139,7 @@ impl PublicInferenceJobType {
|
||||
Self::ActingFace => InferenceJobType::LivePortrait,
|
||||
Self::Lipsync => InferenceJobType::FaceFusion,
|
||||
// Conserved variants
|
||||
Self::FalQueue => InferenceJobType::FalQueue,
|
||||
Self::VideoRender => InferenceJobType::VideoRender,
|
||||
Self::GptSovits => InferenceJobType::GptSovits,
|
||||
Self::F5TTS => InferenceJobType::F5TTS,
|
||||
|
||||
@@ -360,6 +360,13 @@ async fn old_dispatch(
|
||||
return Err(ProcessSingleJobError::InvalidJob(
|
||||
anyhow!("invalid job category for dispatch: {:?}", job.inference_category)))
|
||||
}
|
||||
InferenceCategory::VideoGeneration
|
||||
| InferenceCategory::BackgroundRemoval
|
||||
| InferenceCategory::ObjectGeneration => {
|
||||
// These are handled by the new dispatch
|
||||
return Err(ProcessSingleJobError::InvalidJob(
|
||||
anyhow!("these job types do not support the old-style dispatch: {:?}", job.inference_category)))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(job_success_result)
|
||||
|
||||
@@ -31,6 +31,7 @@ workspace-build-acceleration = { workspace = true }
|
||||
# Internal
|
||||
actix_cors_configs = { path = "../../../lib/actix_cors_configs" }
|
||||
actix_helpers = { path = "../../../lib/actix_helpers" }
|
||||
artcraft_api_defs = { workspace = true }
|
||||
bootstrap = { path = "../../../lib/bootstrap" }
|
||||
bucket_paths = { path = "../../../lib/bucket_paths" }
|
||||
cloud_storage = { path = "../../../lib/api_clients/cloud_storage" }
|
||||
@@ -46,6 +47,7 @@ email_sender= { path = "../../../lib/email_sender" }
|
||||
enums = { workspace = true }
|
||||
enums_public = { workspace = true }
|
||||
errors = { workspace = true }
|
||||
fal_client = { workspace = true }
|
||||
filesys = { workspace = true }
|
||||
google_sign_in = { path = "../../../lib/api_clients/google_sign_in" }
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
@@ -122,7 +124,9 @@ elasticsearch = "8.5.0-alpha.1"
|
||||
|
||||
# Files
|
||||
infer = "0.16.0"
|
||||
tempdir = "0.3.7"
|
||||
bytes = { workspace = true }
|
||||
tempdir = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
# The "never-encode-slash" feature seems to have been removed:
|
||||
# https://github.com/durch/rust-s3/pull/254
|
||||
|
||||
@@ -41,3 +41,6 @@ GC_ENABLED_PUBLIC_BUCKET_NAME="REPLACE_ME"
|
||||
# Legacy configs
|
||||
AUDIO_UPLOADS_BUCKET_ROOT=/user_uploaded_w2l_audio
|
||||
|
||||
# Fake API key for FAL
|
||||
FAL_WEBHOOK_URL="https://macaroni-1.tailce84f.ts.net/v1/webhooks/fal"
|
||||
#FAL_WEBHOOK_URL="https://s1.pancake-chimaera.ts.net/api/fal/webhook"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod remove_image_background_handler;
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
|
||||
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::image::remove_background_rembg_webhook::{remove_background_rembg_webhook, RemoveBackgroundRembgWebhookArgs};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
// =============== Error Response ===============
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub enum RemoveImageBackgroundError {
|
||||
BadInput(String),
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for RemoveImageBackgroundError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
RemoveImageBackgroundError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
RemoveImageBackgroundError::NotFound => StatusCode::NOT_FOUND,
|
||||
RemoveImageBackgroundError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
RemoveImageBackgroundError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for RemoveImageBackgroundError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
/// Background removal
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Images",
|
||||
path = "/v1/generate/image/remove_background",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = RemoveImageBackgroundResponse),
|
||||
(status = 400, description = "Bad input", body = RemoveImageBackgroundError),
|
||||
(status = 401, description = "Not authorized", body = RemoveImageBackgroundError),
|
||||
(status = 500, description = "Server error", body = RemoveImageBackgroundError),
|
||||
),
|
||||
params(
|
||||
("request" = RemoveImageBackgroundRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn remove_image_background_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<RemoveImageBackgroundRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<RemoveImageBackgroundResponse>, RemoveImageBackgroundError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
RemoveImageBackgroundError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(RemoveImageBackgroundError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
let media_file_token = match &request.media_file_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
warn!("No media file token provided");
|
||||
return Err(RemoveImageBackgroundError::BadInput("No media file token provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(RemoveImageBackgroundError::BadInput(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
RemoveImageBackgroundError::BadInput("invalid idempotency token".to_string())
|
||||
})?;
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
let media_file_lookup_result = get_media_file(
|
||||
media_file_token,
|
||||
IS_MOD,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let media_file = match media_file_lookup_result {
|
||||
Ok(Some(media_file)) => media_file,
|
||||
Ok(None) => {
|
||||
warn!("MediaFile not found: {:?}", media_file_token);
|
||||
return Err(RemoveImageBackgroundError::NotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error looking up media_file: {:?}", err);
|
||||
return Err(RemoveImageBackgroundError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
|
||||
return Err(RemoveImageBackgroundError::BadInput("Media file must be a JPG or PNG image".to_string()));
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
let bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&media_file.public_bucket_directory_hash,
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let args = RemoveBackgroundRembgWebhookArgs {
|
||||
image_url: media_links.cdn_url,
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
api_key: &server_state.fal.api_key,
|
||||
};
|
||||
|
||||
let fal_result = remove_background_rembg_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling remove_background_rembg_webhook: {:?}", err);
|
||||
RemoveImageBackgroundError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
RemoveImageBackgroundError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::BackgroundRemoval,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(RemoveImageBackgroundError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RemoveImageBackgroundResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod video;
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::object::enqueue_hunyuan2_image_to_3d_webhook::enqueue_hunyuan2_image_to_3d_webhook;
|
||||
use fal_client::requests::webhook::object::enqueue_hunyuan2_image_to_3d_webhook::Hunyuan2Args;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub enum GenerateHunyuan2ImageTo3dError {
|
||||
BadInput(String),
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for GenerateHunyuan2ImageTo3dError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GenerateHunyuan2ImageTo3dError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
GenerateHunyuan2ImageTo3dError::NotFound => StatusCode::NOT_FOUND,
|
||||
GenerateHunyuan2ImageTo3dError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
GenerateHunyuan2ImageTo3dError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GenerateHunyuan2ImageTo3dError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
/// Background removal
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Objects",
|
||||
path = "/v1/generate/object/hunyuan_2_image_to_3d",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GenerateHunyuan2ImageTo3dResponse),
|
||||
(status = 400, description = "Bad input", body = GenerateHunyuan2ImageTo3dError),
|
||||
(status = 401, description = "Not authorized", body = GenerateHunyuan2ImageTo3dError),
|
||||
(status = 500, description = "Server error", body = GenerateHunyuan2ImageTo3dError),
|
||||
),
|
||||
params(
|
||||
("request" = GenerateHunyuan2ImageTo3dRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn generate_hunyuan_2_image_to_3d_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateHunyuan2ImageTo3dRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateHunyuan2ImageTo3dResponse>, GenerateHunyuan2ImageTo3dError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
GenerateHunyuan2ImageTo3dError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(GenerateHunyuan2ImageTo3dError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
let media_file_token = match &request.media_file_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
warn!("No media file token provided");
|
||||
return Err(GenerateHunyuan2ImageTo3dError::BadInput("No media file token provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(GenerateHunyuan2ImageTo3dError::BadInput(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
GenerateHunyuan2ImageTo3dError::BadInput("invalid idempotency token".to_string())
|
||||
})?;
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
let media_file_lookup_result = get_media_file(
|
||||
media_file_token,
|
||||
IS_MOD,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let media_file = match media_file_lookup_result {
|
||||
Ok(Some(media_file)) => media_file,
|
||||
Ok(None) => {
|
||||
warn!("MediaFile not found: {:?}", media_file_token);
|
||||
return Err(GenerateHunyuan2ImageTo3dError::NotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error looking up media_file: {:?}", err);
|
||||
return Err(GenerateHunyuan2ImageTo3dError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
|
||||
return Err(GenerateHunyuan2ImageTo3dError::BadInput("Media file must be a JPG or PNG image".to_string()));
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
let bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&media_file.public_bucket_directory_hash,
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let prompt = request.prompt
|
||||
.as_deref()
|
||||
.map(|prompt| prompt.trim())
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
let args = Hunyuan2Args {
|
||||
image_url: media_links.cdn_url,
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
api_key: &server_state.fal.api_key,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_hunyuan2_image_to_3d_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_hunyuan2_image_to_3d_webhook: {:?}", err);
|
||||
GenerateHunyuan2ImageTo3dError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
GenerateHunyuan2ImageTo3dError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ObjectGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(GenerateHunyuan2ImageTo3dError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(GenerateHunyuan2ImageTo3dResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate_hunyuan2_image_to_3d_handler;
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_image_to_video::GenerateKling16ImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_image_to_video::GenerateKling16ImageToVideoResponse;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::enqueue_kling_16_image_to_video_webhook;
|
||||
use fal_client::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::Kling16Args;
|
||||
use fal_client::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::Kling16Duration;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub enum GenerateKling16VideoError {
|
||||
BadInput(String),
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for GenerateKling16VideoError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GenerateKling16VideoError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
GenerateKling16VideoError::NotFound => StatusCode::NOT_FOUND,
|
||||
GenerateKling16VideoError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
GenerateKling16VideoError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GenerateKling16VideoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
/// Background removal
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Videos",
|
||||
path = "/v1/generate/video/kling_16_image_to_video",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = RemoveImageBackgroundResponse),
|
||||
(status = 400, description = "Bad input", body = RemoveImageBackgroundError),
|
||||
(status = 401, description = "Not authorized", body = RemoveImageBackgroundError),
|
||||
(status = 500, description = "Server error", body = RemoveImageBackgroundError),
|
||||
),
|
||||
params(
|
||||
("request" = RemoveImageBackgroundRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn generate_kling_1_6_video_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateKling16ImageToVideoRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateKling16ImageToVideoResponse>, GenerateKling16VideoError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
GenerateKling16VideoError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(RemoveImageBackgroundError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
let media_file_token = match &request.media_file_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
warn!("No media file token provided");
|
||||
return Err(GenerateKling16VideoError::BadInput("No media file token provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(GenerateKling16VideoError::BadInput(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
GenerateKling16VideoError::BadInput("invalid idempotency token".to_string())
|
||||
})?;
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
let media_file_lookup_result = get_media_file(
|
||||
media_file_token,
|
||||
IS_MOD,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let media_file = match media_file_lookup_result {
|
||||
Ok(Some(media_file)) => media_file,
|
||||
Ok(None) => {
|
||||
warn!("MediaFile not found: {:?}", media_file_token);
|
||||
return Err(GenerateKling16VideoError::NotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error looking up media_file: {:?}", err);
|
||||
return Err(GenerateKling16VideoError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
|
||||
return Err(GenerateKling16VideoError::BadInput("Media file must be a JPG or PNG image".to_string()));
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
let bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&media_file.public_bucket_directory_hash,
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let prompt = request.prompt
|
||||
.as_deref()
|
||||
.map(|prompt| prompt.trim())
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
let args = Kling16Args {
|
||||
image_url: media_links.cdn_url,
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
duration: Kling16Duration::Default,
|
||||
prompt: prompt,
|
||||
api_key: &server_state.fal.api_key,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_kling_16_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling remove_background_rembg_webhook: {:?}", err);
|
||||
GenerateKling16VideoError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
GenerateKling16VideoError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(GenerateKling16VideoError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(GenerateKling16ImageToVideoResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate_kling_1_6_video_handler;
|
||||
+5
@@ -70,6 +70,11 @@ pub fn estimate_job_progress(job: &GenericInferenceJobStatus, maybe_args: Option
|
||||
InferenceCategory::F5TTS => percent(duration_seconds, F5_TTS_JOB_AVERAGE_SECONDS),
|
||||
InferenceCategory::SeedVc => percent(duration_seconds, SEED_VC_JOB_AVERAGE_SECONDS),
|
||||
|
||||
// TODO: Better estimates for FAL, etc.
|
||||
InferenceCategory::VideoGeneration => percent(duration_seconds, 60*5),
|
||||
InferenceCategory::BackgroundRemoval => percent(duration_seconds, 10),
|
||||
InferenceCategory::ObjectGeneration => percent(duration_seconds, 60*2),
|
||||
|
||||
// TODO: Better estimate using video duration, params, etc.
|
||||
InferenceCategory::Workflow => comfy_workflow_estimate(maybe_args, duration_seconds),
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod beta_keys;
|
||||
pub mod comments;
|
||||
pub mod download_job;
|
||||
pub mod featured_items;
|
||||
pub mod generate;
|
||||
pub mod image_studio;
|
||||
pub mod inference_job;
|
||||
pub mod media_files;
|
||||
@@ -18,10 +19,11 @@ pub mod subscriptions;
|
||||
pub mod tags;
|
||||
pub mod trending;
|
||||
pub mod tts;
|
||||
pub mod users;
|
||||
pub mod user_bookmarks;
|
||||
pub mod user_ratings;
|
||||
pub mod users;
|
||||
pub mod voice_clone_requests;
|
||||
pub mod voice_conversion;
|
||||
pub mod voice_designer;
|
||||
pub mod webhooks;
|
||||
pub mod weights;
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::endpoints::webhooks::handle_image_payload::handle_image_payload;
|
||||
use crate::http_server::endpoints::webhooks::handle_model_mesh_payload::handle_model_mesh_payload;
|
||||
use crate::http_server::endpoints::webhooks::handle_video_payload::handle_video_payload;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use anyhow::Error;
|
||||
use errors::AnyhowResult;
|
||||
use http_server_common::response::response_success_helpers::SimpleGenericJsonSuccess;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use log::{info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id;
|
||||
use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::mark_fal_generic_inference_job_successfully_done;
|
||||
use serde_json::Value;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
// 1. tauri --> hit endpoint to enqueue
|
||||
//
|
||||
// 2. webhook
|
||||
// - upload media file
|
||||
// - update job record with media token + status
|
||||
//
|
||||
// 3. tauri --> storyteller jobs endpoint polls
|
||||
// - alert frontend of completion
|
||||
//
|
||||
// 4. javascript polls tauri (backend removal)
|
||||
|
||||
// TODO(bt, 2025-06-03): Handle webhook crypto authentication
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct FalWebhookRequest {
|
||||
pub status: FalWebhookStatus,
|
||||
|
||||
pub request_id: Option<String>,
|
||||
pub gateway_request_id: Option<String>,
|
||||
|
||||
pub error: Option<String>,
|
||||
|
||||
/// Payload of the webhook, if any.
|
||||
pub payload: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub enum FalWebhookStatus {
|
||||
#[serde(alias = "OK")]
|
||||
Ok,
|
||||
#[serde(alias = "ERROR")]
|
||||
Error
|
||||
}
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub enum FalWebhookError {
|
||||
BadInput(String),
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for FalWebhookError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
FalWebhookError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
FalWebhookError::NotFound => StatusCode::NOT_FOUND,
|
||||
FalWebhookError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
FalWebhookError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for FalWebhookError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for FalWebhookError {
|
||||
fn from(value: Error) -> Self {
|
||||
info!("Converting anyhow::Error to FalWebhookError: {:?}", value);
|
||||
FalWebhookError::ServerError
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
/// Fal webhook
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Webooks",
|
||||
path = "/v1/webhooks/fal",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = SimpleGenericJsonSuccess),
|
||||
(status = 400, description = "Bad input", body = FalWebhookError),
|
||||
(status = 401, description = "Not authorized", body = FalWebhookError),
|
||||
(status = 500, description = "Server error", body = FalWebhookError),
|
||||
),
|
||||
params(
|
||||
("request" = FalWebhookRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn fal_webhook_handler(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
request: Json<FalWebhookRequest>,
|
||||
) -> Result<Json<SimpleGenericJsonSuccess>, FalWebhookError> {
|
||||
|
||||
info!("Received FAL webhook body: {:?}", request);
|
||||
|
||||
let request_id = request.request_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| FalWebhookError::BadInput("Missing request_id".to_string()))?;
|
||||
|
||||
let payload = request.payload
|
||||
.as_ref()
|
||||
.ok_or_else(|| FalWebhookError::BadInput("Missing payload".to_string()))?;
|
||||
|
||||
let db_result = get_inference_job_by_fal_id(
|
||||
request_id,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let job = match db_result {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => {
|
||||
warn!("Could not find job record by fal request id: {:?} ; request payload = {:?}", request_id, request);
|
||||
return Err(FalWebhookError::NotFound)
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error querying job record: {:?}", err);
|
||||
return Err(FalWebhookError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let mut maybe_media_token = None;
|
||||
|
||||
if let Some(payload_obj) = payload.as_object() {
|
||||
if payload_obj.contains_key("image") {
|
||||
let token = handle_image_payload(payload_obj, &job, &server_state).await?;
|
||||
maybe_media_token = Some(token);
|
||||
} else if payload_obj.contains_key("video") {
|
||||
let token = handle_video_payload(payload_obj, &job, &server_state).await?;
|
||||
maybe_media_token = Some(token);
|
||||
} else if payload_obj.contains_key("model_mesh") {
|
||||
let token = handle_model_mesh_payload(payload_obj, &job, &server_state).await?;
|
||||
maybe_media_token = Some(token);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(media_token) = maybe_media_token {
|
||||
info!("Media file token: {:?}", media_token);
|
||||
// TODO: Update job metadata.
|
||||
mark_fal_generic_inference_job_successfully_done(
|
||||
&server_state.mysql_pool,
|
||||
&job.job_token,
|
||||
media_token,
|
||||
).await.map_err(|err| {
|
||||
warn!("Error marking job as successfully done: {:?}", err);
|
||||
FalWebhookError::ServerError
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(SimpleGenericJsonSuccess::wrapped(true))
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use crate::util::http_download_url_to_tempfile::http_download_url_to_tempfile;
|
||||
use actix_web::web;
|
||||
use anyhow::anyhow;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use images::image_info::image_info::ImageInfo;
|
||||
use log::{info, warn};
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use serde_json::{Map, Value};
|
||||
use std::sync::Arc;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
const PREFIX : Option<&str> = Some("artcraft_");
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct FalWebhookImage {
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub height: Option<usize>,
|
||||
pub width: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn handle_image_payload(
|
||||
payload: &Map<String, Value>,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
let image_value = payload.get("image")
|
||||
.ok_or_else(|| anyhow!("no `image` key in payload"))?;
|
||||
|
||||
info!("Fal Image Payload: {:?}", image_value);
|
||||
|
||||
let image: FalWebhookImage = serde_json::from_value(image_value.clone())?;
|
||||
|
||||
let image_url = image.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
|
||||
|
||||
//let mime_type = image.content_type
|
||||
// .as_deref()
|
||||
// .ok_or_else(|| anyhow!("no `content_type` in image payload"))?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(image_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
let image_info = ImageInfo::decode_image_from_bytes(&file_bytes)?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
|
||||
|
||||
server_state.public_bucket_client.upload_file_with_content_type_process(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.creator_ip_address(&job.creator_ip_address)
|
||||
.public_bucket_directory_hash(&public_upload_path)
|
||||
.media_file_class(MediaFileClass::Image)
|
||||
.media_file_type(media_file_type)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.frame_width(image_info.width())
|
||||
.frame_height(image_info.height())
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
info!("Image media file uploaded with token: {}", media_token);
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use crate::util::http_download_url_to_tempfile::http_download_url_to_tempfile;
|
||||
use actix_web::web;
|
||||
use anyhow::anyhow;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use images::image_info::image_info::ImageInfo;
|
||||
use log::{info, warn};
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use serde_json::{Map, Value};
|
||||
use std::sync::Arc;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
const PREFIX : Option<&str> = Some("artcraft_");
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct FalWebhookModelMesh {
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn handle_model_mesh_payload(
|
||||
payload: &Map<String, Value>,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
let model_mesh_value = payload.get("model_mesh")
|
||||
.ok_or_else(|| anyhow!("no `model_mesh` key in payload"))?;
|
||||
|
||||
info!("Fal Model Mesh Payload: {:?}", model_mesh_value);
|
||||
|
||||
let mesh: FalWebhookModelMesh = serde_json::from_value(model_mesh_value.clone())?;
|
||||
|
||||
let mesh_url = mesh.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
|
||||
|
||||
//let mime_type = mesh.content_type
|
||||
// .as_deref()
|
||||
// .ok_or_else(|| anyhow!("no `content_type` in mesh payload"))?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download mesh: {:?}", e))?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
|
||||
info!("Mime type of mesh: {}", mime_type);
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
|
||||
|
||||
server_state.public_bucket_client.upload_file_with_content_type_process(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.creator_ip_address(&job.creator_ip_address)
|
||||
.public_bucket_directory_hash(&public_upload_path)
|
||||
.media_file_class(MediaFileClass::Dimensional)
|
||||
.media_file_type(media_file_type)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
info!("Mesh media file uploaded with token: {}", media_token);
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use crate::util::http_download_url_to_tempfile::http_download_url_to_tempfile;
|
||||
use actix_multipart::form::tempfile::TempFile;
|
||||
use actix_web::web;
|
||||
use anyhow::anyhow;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use images::image_info::image_info::ImageInfo;
|
||||
use log::{info, warn};
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use serde_json::{Map, Value};
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use videos::ffprobe_get_info::ffprobe_get_info;
|
||||
|
||||
const PREFIX : Option<&str> = Some("artcraft_");
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct FalWebhookVideo {
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub height: Option<usize>, // NB: Might not be on videos
|
||||
pub width: Option<usize>, // NB: Might not be on videos
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn handle_video_payload(
|
||||
payload: &Map<String, Value>,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
let video_value = payload.get("video")
|
||||
.ok_or_else(|| anyhow!("no `image` key in payload"))?;
|
||||
|
||||
info!("Fal Video Payload: {:?}", video_value);
|
||||
|
||||
let video: FalWebhookVideo = serde_json::from_value(video_value.clone())?;
|
||||
|
||||
let video_url = video.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in video payload"))?;
|
||||
|
||||
//let mime_type = image.content_type
|
||||
// .as_deref()
|
||||
// .ok_or_else(|| anyhow!("no `content_type` in image payload"))?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(video_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
|
||||
let mut maybe_duration_millis = None;
|
||||
let mut maybe_frame_width = None;
|
||||
let mut maybe_frame_height = None;
|
||||
|
||||
if let Ok(mut file) = NamedTempFile::new() {
|
||||
// NB: Fallible
|
||||
let _r = file.write_all(&file_bytes);
|
||||
|
||||
if let Ok(video_info) = ffprobe_get_info(&file.path()) {
|
||||
maybe_duration_millis = video_info.duration
|
||||
.as_ref()
|
||||
.map(|duration| duration.millis as u64);
|
||||
|
||||
maybe_frame_width = video_info.dimensions
|
||||
.as_ref()
|
||||
.map(|ref dims| dims.width as u32);
|
||||
|
||||
maybe_frame_height = video_info.dimensions
|
||||
.as_ref()
|
||||
.map(|dims| dims.height as u32);
|
||||
}
|
||||
}
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
|
||||
|
||||
server_state.public_bucket_client.upload_file_with_content_type_process(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.creator_ip_address(&job.creator_ip_address)
|
||||
.public_bucket_directory_hash(&public_upload_path)
|
||||
.media_file_class(MediaFileClass::Image)
|
||||
.media_file_type(media_file_type)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.maybe_frame_width(maybe_frame_width)
|
||||
.maybe_frame_height(maybe_frame_height)
|
||||
.maybe_duration_millis(maybe_duration_millis)
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
info!("Video media file uploaded with token: {}", media_token);
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod fal_webhook_handler;
|
||||
pub mod handle_image_payload;
|
||||
pub mod handle_model_mesh_payload;
|
||||
pub mod handle_video_payload;
|
||||
@@ -0,0 +1,40 @@
|
||||
use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler;
|
||||
use crate::http_server::endpoints::generate::object::generate_hunyuan2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler;
|
||||
use crate::http_server::endpoints::generate::video::generate_kling_1_6_video_handler::generate_kling_1_6_video_handler;
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
pub fn add_generate_routes<T, B> (app: App<T>) -> App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
app.service(web::scope("/v1/generate")
|
||||
.service(web::scope("/image")
|
||||
.service(web::resource("/remove_background")
|
||||
.route(web::post().to(remove_image_background_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::scope("/video")
|
||||
.service(web::resource("/kling_16_image_to_video")
|
||||
.route(web::post().to(generate_kling_1_6_video_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::scope("/object")
|
||||
.service(web::resource("/hunyuan_2_image_to_3d")
|
||||
.route(web::post().to(generate_hunyuan_2_image_to_3d_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -124,8 +124,10 @@ use crate::http_server::endpoints::voice_designer::voices::list_voices_by_user::
|
||||
use crate::http_server::endpoints::voice_designer::voices::search_voices::search_voices;
|
||||
use crate::http_server::endpoints::voice_designer::voices::update_voice::update_voice_handler;
|
||||
use crate::http_server::routes::add_control_plane_routes::add_control_plane_routes;
|
||||
use crate::http_server::routes::add_generate_routes::add_generate_routes;
|
||||
use crate::http_server::routes::add_image_studio_routes::add_image_studio_routes;
|
||||
use crate::http_server::routes::add_studio_gen2_routes::add_studio_gen2_routes;
|
||||
use crate::http_server::routes::add_webhook_routes::add_webhook_routes;
|
||||
use crate::http_server::routes::beta_key_routes::add_beta_key_routes;
|
||||
use crate::http_server::routes::job_routes::add_job_routes;
|
||||
use crate::http_server::routes::media_files_routes::add_media_file_routes;
|
||||
@@ -177,6 +179,8 @@ pub fn add_routes<T, B> (app: App<T>, server_environment: ServerEnvironment) ->
|
||||
app = add_job_routes(app);
|
||||
app = add_engine_routes(app); /* /v1/engine/... */
|
||||
app = add_control_plane_routes(app); /* /v1/control_plane/... */
|
||||
app = add_generate_routes(app); /* /v1/generate/... */
|
||||
app = add_webhook_routes(app); /* /v1/webhooks/... */
|
||||
|
||||
// app = add_image_gen_routes(app);
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use crate::http_server::endpoints::webhooks::fal_webhook_handler::fal_webhook_handler;
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
pub fn add_webhook_routes<T, B> (app: App<T>) -> App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
app.service(web::scope("/v1/webhooks")
|
||||
.service(web::resource("/fal")
|
||||
.route(web::post().to(fal_webhook_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
pub mod add_routes;
|
||||
pub mod add_studio_gen2_routes;
|
||||
pub mod beta_key_routes;
|
||||
pub mod job_routes;
|
||||
pub mod media_files_routes;
|
||||
pub mod model_download_routes;
|
||||
pub mod moderation_routes;
|
||||
pub mod user_routes;
|
||||
pub mod weights_routes;
|
||||
pub mod workflow_routes;
|
||||
mod tag_routes;
|
||||
mod add_control_plane_routes;
|
||||
mod add_generate_routes;
|
||||
mod add_image_studio_routes;
|
||||
mod add_control_plane_routes;
|
||||
mod add_studio_gen2_routes;
|
||||
mod add_webhook_routes;
|
||||
mod beta_key_routes;
|
||||
mod job_routes;
|
||||
mod media_files_routes;
|
||||
mod model_download_routes;
|
||||
mod moderation_routes;
|
||||
mod tag_routes;
|
||||
mod user_routes;
|
||||
mod weights_routes;
|
||||
mod workflow_routes;
|
||||
pub mod add_routes;
|
||||
@@ -60,6 +60,7 @@ use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING;
|
||||
use config::shared_constants::DEFAULT_RUST_LOG;
|
||||
use email_sender::smtp_email_sender::SmtpEmailSender;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use memory_caching::arc_ttl_sieve::ArcTtlSieve;
|
||||
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
|
||||
use mysql_queries::mediators::badge_granter::BadgeGranter;
|
||||
@@ -85,7 +86,7 @@ use crate::http_server::web_utils::handle_multipart_error::handle_multipart_erro
|
||||
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
|
||||
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, InMemoryCaches, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans};
|
||||
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans};
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread;
|
||||
use crate::threads::poll_ip_banlist_thread::poll_ip_bans;
|
||||
@@ -391,6 +392,9 @@ async fn main() -> AnyhowResult<()> {
|
||||
ServerEnvironment::Production => server_environment::ServerEnvironment::Production,
|
||||
ServerEnvironment::Development => server_environment::ServerEnvironment::Development,
|
||||
};
|
||||
|
||||
let fal_api_key = FalApiKey::new(easyenv::get_env_string_required("FAL_API_KEY")?);
|
||||
let fal_webhook_url = easyenv::get_env_string_required("FAL_WEBHOOK_URL")?;
|
||||
|
||||
let server_state = ServerState {
|
||||
env_config: EnvConfig {
|
||||
@@ -431,6 +435,10 @@ async fn main() -> AnyhowResult<()> {
|
||||
sort_key_crypto,
|
||||
static_api_token_set,
|
||||
email_sender,
|
||||
fal: FalData {
|
||||
api_key: fal_api_key,
|
||||
webhook_url: fal_webhook_url,
|
||||
},
|
||||
caches: InMemoryCaches {
|
||||
durable: DurableInMemoryCaches {
|
||||
model_token_info: model_token_info_cache,
|
||||
|
||||
@@ -7,6 +7,7 @@ use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBan
|
||||
use billing_component::stripe::stripe_config::StripeConfig;
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use email_sender::smtp_email_sender::SmtpEmailSender;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use memory_caching::arc_ttl_sieve::ArcTtlSieve;
|
||||
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
|
||||
use mysql_queries::mediators::badge_granter::BadgeGranter;
|
||||
@@ -85,6 +86,8 @@ pub struct ServerState {
|
||||
pub public_bucket_client: BucketClient,
|
||||
pub auto_gc_bucket_client: BucketClient,
|
||||
|
||||
pub fal: FalData,
|
||||
|
||||
/// Where to store audio uploads for w2l
|
||||
pub audio_uploads_bucket_root: String,
|
||||
|
||||
@@ -283,3 +286,10 @@ pub struct TrollBans {
|
||||
pub user_tokens: TrollUserBanList,
|
||||
pub ip_addresses: IpBanList,
|
||||
}
|
||||
|
||||
/// Fal integration
|
||||
#[derive(Clone)]
|
||||
pub struct FalData {
|
||||
pub api_key: FalApiKey,
|
||||
pub webhook_url: String,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use bytes::Bytes;
|
||||
use errors::AnyhowResult;
|
||||
use reqwest::{Client, IntoUrl};
|
||||
|
||||
const USER_AGENT: &str = "storyteller-web/1.0";
|
||||
|
||||
// TODO(bt, 2025-06-03): Don't load the entire file into memory!
|
||||
|
||||
/// Downloads a (binary) file to memory. Good for images, etc. Not great for large files.
|
||||
pub async fn http_download_url_to_bytes<U: IntoUrl>(url: U) -> AnyhowResult<Bytes> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(url) // NB: No IntoUrl for &Url.
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", "*/*")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use errors::AnyhowResult;
|
||||
use reqwest::IntoUrl;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
const USER_AGENT: &str = "storyteller-web/1.0";
|
||||
|
||||
/// Downloads a (binary) file to a filesystem path. Good for images, etc. Not great for large files.
|
||||
pub async fn http_download_url_to_tempfile<U: IntoUrl>(url: U, temp_file: &mut NamedTempFile) -> AnyhowResult<()> {
|
||||
let bytes = http_download_url_to_bytes(url).await?;
|
||||
temp_file.write_all(&bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,6 +9,8 @@ pub mod email_to_gravatar;
|
||||
pub mod encrypted_sort_id;
|
||||
pub mod enroll_in_studio;
|
||||
pub mod generate_random_username;
|
||||
pub mod http_download_url_to_bytes;
|
||||
pub mod http_download_url_to_tempfile;
|
||||
pub mod placeholder_images;
|
||||
pub mod read_toml_file_to_struct;
|
||||
pub mod title_to_url_slug;
|
||||
|
||||
Vendored
+2
@@ -33,6 +33,8 @@ all-features = true
|
||||
eventsource-stream = "0.2"
|
||||
smart-default = "0.7"
|
||||
thiserror = "1"
|
||||
percent-encoding-rfc3986 = "0.1.3"
|
||||
log = "0.4.27"
|
||||
|
||||
[dependencies.base64]
|
||||
version = "0.22"
|
||||
|
||||
Vendored
+1
@@ -7,6 +7,7 @@ pub mod endpoints;
|
||||
pub mod image;
|
||||
pub mod queue;
|
||||
pub mod request;
|
||||
pub mod webhook;
|
||||
|
||||
use eventsource_stream::EventStreamError;
|
||||
pub use fal_derive::endpoint;
|
||||
|
||||
Vendored
+35
-2
@@ -1,11 +1,13 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use percent_encoding_rfc3986::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use reqwest::IntoUrl;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use log::info;
|
||||
use crate::{
|
||||
queue::{Queue, QueueResponse},
|
||||
FalError,
|
||||
};
|
||||
use crate::webhook::WebhookResponse;
|
||||
|
||||
/// A request to the FAL API
|
||||
///
|
||||
@@ -129,4 +131,35 @@ impl<Params: Serialize, Response: DeserializeOwned> FalRequest<Params, Response>
|
||||
|
||||
Ok(Queue::new(self.client, self.endpoint, key, payload))
|
||||
}
|
||||
|
||||
pub async fn queue_webhook<U: IntoUrl>(self, url: U) -> Result<WebhookResponse, FalError> {
|
||||
let key = self
|
||||
.api_key
|
||||
.expect("No fal API key provided, and FAL_API_KEY environment variable is not set");
|
||||
|
||||
let url_encoded = url.into_url()?;
|
||||
//let url_encoded = utf8_percent_encode(url_encoded.as_str(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let request_url = format!("https://queue.fal.run/{}?fal_webhook={}", self.endpoint, url_encoded);
|
||||
|
||||
info!("Sending request to FAL queue webhook: {}", request_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(request_url)
|
||||
.json(&self.params)
|
||||
.header("Authorization", format!("Key {}", &key))
|
||||
.header("Content-Type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let error = response.text().await?;
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
let payload: WebhookResponse = response.error_for_status()?.json().await?;
|
||||
|
||||
Ok(payload)
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
|
||||
use crate::prelude::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct WebhookResponse {
|
||||
pub status: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
pub gateway_request_id: Option<String>,
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Footer from "../../components/footer";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.06.04.dmg";
|
||||
|
||||
const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.06.04.exe";
|
||||
|
||||
const fadeUpVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
@@ -87,8 +91,8 @@ const Landing = () => {
|
||||
animate="visible"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
Your vision, amplified. Create stunning art with unparalleled AI
|
||||
control and precision.
|
||||
Text prompting sucks. Bring your true vision to life with
|
||||
unparalleled control and precision.
|
||||
</motion.p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
@@ -110,7 +114,7 @@ const Landing = () => {
|
||||
<Button
|
||||
className="text-md px-4 py-3 font-semibold rounded-xl shadow-lg gap-3"
|
||||
as="link"
|
||||
href="/"
|
||||
href={WINDOWS_LINK}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWindows} />
|
||||
Download Windows
|
||||
@@ -118,7 +122,7 @@ const Landing = () => {
|
||||
<Button
|
||||
className="text-md px-4 py-3 font-semibold rounded-xl shadow-lg gap-3"
|
||||
as="link"
|
||||
href="/"
|
||||
href={MAC_LINK}
|
||||
>
|
||||
<FontAwesomeIcon icon={faApple} />
|
||||
Download Mac
|
||||
@@ -259,7 +263,7 @@ const Landing = () => {
|
||||
<div className="max-w-2xl lg:mx-0 mb-10 md:mb-16">
|
||||
<h2 className="relative font-bold text-4xl md:text-5xl lg:text-6xl !leading-tight">
|
||||
Our tool lets you design, control, and achieve{" "}
|
||||
<span className="text-primary">great consistency.</span>
|
||||
<span className="text-primary">perfect consistency</span>
|
||||
</h2>
|
||||
</div>
|
||||
<DiscordButton />
|
||||
@@ -317,7 +321,7 @@ const Landing = () => {
|
||||
<div className=" max-w-2xl lg:mx-0 mb-10 md:mb-16">
|
||||
<h2 className="relative font-bold text-4xl md:text-5xl lg:text-6xl xl:text-[3.8rem] !leading-tight">
|
||||
<span className="text-primary">Patented</span> Brain to Art
|
||||
Interface
|
||||
Interface lets you Sketch and Composite
|
||||
</h2>
|
||||
</div>
|
||||
<DiscordButton />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faGear,
|
||||
faImages,
|
||||
@@ -23,22 +23,21 @@ import {
|
||||
MenuIconSelector,
|
||||
MenuIconItem,
|
||||
} from "@storyteller/ui-menu-icon-selector";
|
||||
import { Signal, signal } from "@preact/signals-react";
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { setLogoutStates } from "~/signals/authentication/utilities";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useTabStore } from "~/pages/Stores/TabState";
|
||||
import {
|
||||
is3DEditorInitialized,
|
||||
is3DSceneLoaded,
|
||||
set3DPageMounted,
|
||||
} from "~/pages/PageEnigma/Editor/editor";
|
||||
interface Props {
|
||||
pageName: string;
|
||||
appTabIdSignal: Signal<string>;
|
||||
setAppTabId: (id: string) => void;
|
||||
is3DInitSignal: Signal<boolean>;
|
||||
}
|
||||
|
||||
// This is the tab we start with.
|
||||
export const STARTING_APP_TAB_ID = "2D";
|
||||
|
||||
const appMenuTabs: MenuIconItem[] = [
|
||||
{
|
||||
id: "2D",
|
||||
@@ -65,75 +64,28 @@ const appMenuTabs: MenuIconItem[] = [
|
||||
export const topNavMediaId = signal<string>("");
|
||||
export const topNavMediaUrl = signal<string>("");
|
||||
|
||||
export const TopBar = ({
|
||||
pageName,
|
||||
appTabIdSignal,
|
||||
setAppTabId,
|
||||
is3DInitSignal,
|
||||
}: Props) => {
|
||||
export const TopBar = ({ pageName }: Props) => {
|
||||
useSignals();
|
||||
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
|
||||
const engine3D = useContext(EngineContext);
|
||||
const handleTabChange = (tabId: string) => {
|
||||
if (appTabIdSignal.peek() == "3D") {
|
||||
engine3D?.unmountEngine();
|
||||
}
|
||||
setAppTabId(tabId);
|
||||
};
|
||||
|
||||
const currentAppTabId = appTabIdSignal.value;
|
||||
const is3DInit = is3DInitSignal.value;
|
||||
useEffect(() => {
|
||||
if (currentAppTabId == "3D" && is3DInit) {
|
||||
engine3D?.remountEngine();
|
||||
}
|
||||
}, [currentAppTabId, engine3D, is3DInit]);
|
||||
|
||||
const handleAddToScene = async (
|
||||
url: string,
|
||||
media_id: string | undefined,
|
||||
) => {
|
||||
console.log("Items to add to scene", currentAppTabId);
|
||||
console.log("url", url);
|
||||
console.log("media_id", media_id);
|
||||
|
||||
setUrl(url);
|
||||
setMediaId(media_id ?? "");
|
||||
|
||||
if (currentAppTabId === "2D") {
|
||||
console.log("Adding to 2D scene");
|
||||
// from the uploaded image url.
|
||||
} else if (currentAppTabId === "3D") {
|
||||
console.log("Adding to 3D scene");
|
||||
// media id from the image selected from gallery.
|
||||
if (media_id) {
|
||||
engine3D?.activeScene.loadObject(media_id, "image", true);
|
||||
} else {
|
||||
console.warn("No media id provided");
|
||||
}
|
||||
} else if (currentAppTabId === "VIDEO") {
|
||||
console.log("Adding to Video scene");
|
||||
topNavMediaId.value = media_id ?? "";
|
||||
topNavMediaUrl.value = url;
|
||||
} else if (currentAppTabId === "IMAGE") {
|
||||
console.log("Adding to Image scene");
|
||||
topNavMediaId.value = media_id ?? "";
|
||||
topNavMediaUrl.value = url;
|
||||
} else {
|
||||
console.warn(`Unknown tab type: ${currentAppTabId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenGalleryModal = () => {
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
};
|
||||
|
||||
const tabStore = useTabStore();
|
||||
|
||||
const is3DSceneReady = is3DSceneLoaded.value;
|
||||
const is3DEditorReady = is3DEditorInitialized.value;
|
||||
const disableTabSwitcher = () => {
|
||||
return (
|
||||
useTabStore.getState().activeTabId === "3D" &&
|
||||
!is3DEditorReady &&
|
||||
!is3DSceneReady
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
|
||||
@@ -152,21 +104,29 @@ export const TopBar = ({
|
||||
</a>
|
||||
<MenuIconSelector
|
||||
menuItems={appMenuTabs}
|
||||
activeMenu={appTabIdSignal.value}
|
||||
disabled={false}
|
||||
onMenuChange={handleTabChange}
|
||||
activeMenu={tabStore.activeTabId}
|
||||
disabled={disableTabSwitcher()}
|
||||
onMenuChange={(tabId) => {
|
||||
// Disable 3d engine to prevent memory leak.
|
||||
if (tabId === "3D") {
|
||||
set3DPageMounted(true);
|
||||
} else {
|
||||
set3DPageMounted(false);
|
||||
}
|
||||
useTabStore.getState().setActiveTab(tabId);
|
||||
}}
|
||||
className="w-fit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 font-medium">
|
||||
{currentAppTabId === "3D" ? (
|
||||
{tabStore.activeTabId === "3D" ? (
|
||||
<SceneTitleInput pageName={pageName} />
|
||||
) : (
|
||||
<h1>
|
||||
{currentAppTabId === "2D"
|
||||
{tabStore.activeTabId === "2D"
|
||||
? "Canvas"
|
||||
: currentAppTabId === "VIDEO"
|
||||
: tabStore.activeTabId === "VIDEO"
|
||||
? "Generate Video"
|
||||
: "Generate Image"}
|
||||
</h1>
|
||||
@@ -206,11 +166,7 @@ export const TopBar = ({
|
||||
globalAccountLogoutCallback={() => setLogoutStates()}
|
||||
/>
|
||||
|
||||
<GalleryModal
|
||||
mode="view"
|
||||
onDownloadClicked={downloadFileFromUrl}
|
||||
onAddToSceneClicked={handleAddToScene}
|
||||
/>
|
||||
<GalleryModal mode="view" onDownloadClicked={downloadFileFromUrl} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { MouseEventHandler, useState, useEffect } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { uiAccess } from "./signals/uiAccess";
|
||||
import { dispatchUiEvents } from "./signals/uiEvents";
|
||||
|
||||
import { ToolbarNode } from "./components/features/ToolbarNode";
|
||||
import { ToolbarNodeButtonNames } from "./components/features/ToolbarNode/enums";
|
||||
import { transitionTimingStyles } from "./components/styles";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const ContextualToolbarNode = () => {
|
||||
const [winSize, setWinSize] = useState<{ width: number; height: number }>({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
const { isShowing, position, ...rest } = uiAccess.toolbarNode.signal.value;
|
||||
const buttonsProps = Object.values(ToolbarNodeButtonNames).reduce(
|
||||
(acc, buttonName) => {
|
||||
acc[buttonName] = {
|
||||
onClick: dispatchUiEvents.toolbarNode[buttonName],
|
||||
disabled:
|
||||
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].disabled,
|
||||
hidden:
|
||||
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].hidden,
|
||||
active:
|
||||
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].active,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as {
|
||||
[key in ToolbarNodeButtonNames]: {
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
disabled: boolean;
|
||||
hidden: boolean;
|
||||
active: boolean;
|
||||
};
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
function resizeHandler() {
|
||||
setWinSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
// Don't do anything if no node is known to be bound
|
||||
if (uiAccess.toolbarNode.signal.value.knodeIds.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the UI component
|
||||
uiAccess.toolbarNode.setColor(color);
|
||||
|
||||
// Signal the engine with the node ID (just grab the first -and expected only- node)
|
||||
const nodeID = uiAccess.toolbarNode.signal.value.knodeIds[0];
|
||||
dispatchUiEvents.toolbarNode.setSelectedColor({ kNodeId: nodeID, color });
|
||||
}
|
||||
|
||||
useSignals();
|
||||
const color = uiAccess.toolbarNode.signal.value.color;
|
||||
|
||||
const boundedTopPosition = Math.min(position.y, winSize.height - 164);
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
show={isShowing}
|
||||
className={twMerge(
|
||||
transitionTimingStyles,
|
||||
"fixed -translate-x-1/2 translate-y-5 data-[closed]:opacity-0",
|
||||
)}
|
||||
style={{
|
||||
top: boundedTopPosition,
|
||||
left: position.x,
|
||||
}}
|
||||
>
|
||||
<ToolbarNode
|
||||
{...rest}
|
||||
buttonsProps={buttonsProps}
|
||||
locked={uiAccess.toolbarNode.signal.value.locked}
|
||||
onLockClicked={(e) => {
|
||||
dispatchUiEvents.toolbarNode.lock(e);
|
||||
}}
|
||||
color={color}
|
||||
onColorChange={handleColorChange}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,767 +0,0 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { uiAccess, uiEvents } from "./signals";
|
||||
import {
|
||||
ImageNode,
|
||||
ShapeNode,
|
||||
ShapeType,
|
||||
TextNode,
|
||||
PaintNode,
|
||||
PreviewCopyNode,
|
||||
} from "./KonvaApp/Nodes";
|
||||
import { UndoStackManager } from "./KonvaApp/UndoRedo";
|
||||
import {
|
||||
CommandManager,
|
||||
MatteBox,
|
||||
SceneManager,
|
||||
} from "./KonvaApp/EngineUtitlities";
|
||||
import {
|
||||
NodeIsolator,
|
||||
NodesManager,
|
||||
NodesTranslationEventDetails,
|
||||
NodeTransformationEventDetails,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
SelectionManagerEvents,
|
||||
SelectorSquare,
|
||||
} from "./KonvaApp/NodesManagers";
|
||||
import { EngineOptions, TextNodeData, VideoNodeData } from "./KonvaApp/types";
|
||||
|
||||
import { AppModes, VideoResolutions } from "./KonvaApp/constants";
|
||||
import { ToolbarMainButtonNames } from "./components/features/ToolbarMain/enum";
|
||||
|
||||
import { ToolbarNodeButtonNames } from "./components/features/ToolbarNode/enums";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
|
||||
import { RealTimeDrawEngine } from "./KonvaApp/RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { NodeColor } from "./signals/uiEvents/toolbarNode";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { DecodeBase64ToImage } from "./utilities/DecodeBase64ToImage";
|
||||
import { EncodeImageBitmapToBase64 } from "./utilities/EncodeImageBitmapToBase64";
|
||||
import {
|
||||
FalBackgroundRemoval,
|
||||
FalBackgroundRemovalRequest,
|
||||
} from "@storyteller/tauri-api";
|
||||
|
||||
export interface RenderingOptions {
|
||||
artstyle: string;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
cinematic: boolean;
|
||||
enginePreProcessing: boolean;
|
||||
faceDetail: boolean;
|
||||
lipSync: boolean;
|
||||
upscale: boolean;
|
||||
styleStrength: number;
|
||||
}
|
||||
|
||||
export class Engine {
|
||||
private navigateRef: NavigateFunction;
|
||||
private appMode: AppModes = AppModes.INIT;
|
||||
private boardCanvasRef: HTMLDivElement;
|
||||
private stage: Konva.Stage;
|
||||
private bgLayer: Konva.Layer;
|
||||
private mediaLayer: Konva.Layer;
|
||||
private nodeIsolationLayer: Konva.Layer;
|
||||
private uiLayer: Konva.Layer;
|
||||
private previewLayer: Konva.Layer;
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
public realTimeDrawEngine: RealTimeDrawEngine;
|
||||
private nodesManager: NodesManager;
|
||||
private nodeIsolator: NodeIsolator;
|
||||
private nodeTransformer: NodeTransformer;
|
||||
private selectionManager: SelectionManager;
|
||||
private selectorSquare: SelectorSquare;
|
||||
private loadingVideosProvider: LoadingVideosProvider;
|
||||
private matteBox: MatteBox;
|
||||
private backgroundNode: ShapeNode | null = null;
|
||||
|
||||
private sceneManager: SceneManager;
|
||||
public undoStackManager: UndoStackManager;
|
||||
public commandManager: CommandManager;
|
||||
|
||||
public segmentationButtonCanBePressed: boolean = true;
|
||||
// signal reference
|
||||
constructor(boardCanvasRef: HTMLDivElement, options: EngineOptions) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Engine Created");
|
||||
}
|
||||
this.navigateRef = options.navigate;
|
||||
this.appMode = AppModes.SELECT;
|
||||
|
||||
this.boardCanvasRef = boardCanvasRef;
|
||||
this.stage = new Konva.Stage({
|
||||
container: this.boardCanvasRef,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
this.bgLayer = new Konva.Layer();
|
||||
|
||||
this.mediaLayer = new Konva.Layer();
|
||||
this.previewLayer = new Konva.Layer();
|
||||
|
||||
this.nodeIsolationLayer = new Konva.Layer();
|
||||
this.uiLayer = new Konva.Layer();
|
||||
|
||||
this.stage.add(this.bgLayer);
|
||||
this.stage.add(this.previewLayer);
|
||||
this.stage.add(this.mediaLayer);
|
||||
this.stage.add(this.nodeIsolationLayer);
|
||||
this.stage.add(this.uiLayer);
|
||||
|
||||
// Konva Transformer
|
||||
this.nodeTransformer = new NodeTransformer();
|
||||
this.uiLayer.add(this.nodeTransformer.getKonvaNode());
|
||||
// Loading Placeholders
|
||||
|
||||
// Node Isolator
|
||||
this.nodeIsolator = new NodeIsolator({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodeIsolationLayerRef: this.nodeIsolationLayer,
|
||||
});
|
||||
|
||||
// core layer for all the work done.
|
||||
this.offScreenCanvas = new OffscreenCanvas(0, 0);
|
||||
|
||||
// Collection of all Nodes
|
||||
this.nodesManager = new NodesManager();
|
||||
// Partial Collection of selected Nodes
|
||||
this.selectionManager = new SelectionManager({
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine = new RealTimeDrawEngine({
|
||||
width: VideoResolutions.SQUARE_1024.width,
|
||||
height: VideoResolutions.SQUARE_1024.height,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
offScreenCanvas: this.offScreenCanvas,
|
||||
onDraw: async (canvas, lineBounds) => {
|
||||
await this.addPaintNode(canvas, lineBounds);
|
||||
},
|
||||
onPreviewCopy: async (image) => {
|
||||
await this.addPreviewCopy(image);
|
||||
},
|
||||
});
|
||||
|
||||
// Selector Square to select Nodes
|
||||
this.selectorSquare = new SelectorSquare({
|
||||
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
stageRef: this.stage,
|
||||
});
|
||||
this.uiLayer.add(this.selectorSquare.getKonvaNode());
|
||||
|
||||
//Collection of commands for undo-redo
|
||||
this.undoStackManager = new UndoStackManager(() => {
|
||||
//this.realTimeDrawEngine.render();
|
||||
});
|
||||
this.commandManager = new CommandManager({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
renderEngineRef: this.realTimeDrawEngine,
|
||||
undoStackManagerRef: this.undoStackManager,
|
||||
});
|
||||
|
||||
// set up secene manager
|
||||
this.sceneManager = new SceneManager({
|
||||
navigateRef: this.navigateRef,
|
||||
loadingVideosProviderRef: this.loadingVideosProvider,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
renderEngineRef: this.realTimeDrawEngine,
|
||||
});
|
||||
|
||||
this.matteBox = new MatteBox({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.clientWidth,
|
||||
height: this.boardCanvasRef.clientHeight,
|
||||
},
|
||||
captureCanvasSize: this.realTimeDrawEngine.captureCanvas.getSize(),
|
||||
uiLayerRef: this.uiLayer,
|
||||
});
|
||||
|
||||
// some of the managers has events
|
||||
// hence, lastly, setup these events
|
||||
this.setupEventSystem();
|
||||
this.setAppMode(this.appMode);
|
||||
}
|
||||
|
||||
private setAppMode(newAppMode: AppModes) {
|
||||
this.appMode = newAppMode;
|
||||
switch (this.appMode) {
|
||||
case AppModes.PAINT: {
|
||||
console.log("APPMODE: PAINT");
|
||||
this.selectorSquare.disable(); // this breaks the paint if you use it in the wrong spot
|
||||
this.selectionManager.disable(); // this prevents selection.
|
||||
this.realTimeDrawEngine.enablePaintMode();
|
||||
|
||||
uiAccess.toolbarMain.enable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
|
||||
active: true,
|
||||
});
|
||||
this.matteBox.disable();
|
||||
return;
|
||||
}
|
||||
case AppModes.SELECT: {
|
||||
console.log("APPMODE: SELECT");
|
||||
this.realTimeDrawEngine.disablePaintMode();
|
||||
|
||||
this.selectorSquare.enable();
|
||||
this.selectionManager.enable();
|
||||
uiAccess.toolbarMain.enable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: true,
|
||||
});
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
|
||||
active: false,
|
||||
});
|
||||
this.matteBox.disable();
|
||||
document.body.style.cursor = "default";
|
||||
return;
|
||||
}
|
||||
case AppModes.PREVIEW: {
|
||||
console.log("APPMODE: RENDER");
|
||||
this.selectorSquare.disable();
|
||||
this.selectionManager.disable();
|
||||
uiAccess.toolbarMain.disable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
this.matteBox.enable();
|
||||
document.body.style.cursor = "not-allowed";
|
||||
return;
|
||||
}
|
||||
case AppModes.RENDERING: {
|
||||
console.log("APPMODE: RENDER");
|
||||
this.selectorSquare.disable();
|
||||
this.selectionManager.disable();
|
||||
uiAccess.toolbarMain.disable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
this.matteBox.enable(true);
|
||||
document.body.style.cursor = "wait";
|
||||
return;
|
||||
}
|
||||
case AppModes.INIT:
|
||||
default: {
|
||||
console.log("APPMODE: INIT");
|
||||
this.selectorSquare.disable();
|
||||
uiAccess.toolbarMain.disable();
|
||||
this.selectionManager.disable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
document.body.style.cursor = "wait";
|
||||
this.matteBox.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
private setupEventSystem() {
|
||||
// Listen to changes in container size
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.onBoardCanvasResize();
|
||||
});
|
||||
resizeObserver.observe(this.boardCanvasRef);
|
||||
this.onBoardCanvasResize();
|
||||
|
||||
// Listen to Nodes
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSLATIONS,
|
||||
((event: CustomEvent<NodesTranslationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.translateNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSFORMATION,
|
||||
((event: CustomEvent<NodeTransformationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.transformNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
|
||||
// Listen to Tooolbar Node
|
||||
uiEvents.toolbarNode.lock.onClick(() => {
|
||||
this.commandManager.toggleLockNodes();
|
||||
});
|
||||
uiEvents.toolbarNode.DOWNLOAD.onClick(() => {
|
||||
const nodes = this.selectionManager.getSelectedNodes();
|
||||
if (nodes.size > 1) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Error: Download Node Content",
|
||||
message:
|
||||
"Please do not select more than 1 item for the Download Node Content feature, you can only download 1 item at a time",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const node = nodes.values().next().value;
|
||||
try {
|
||||
if (node instanceof VideoNode && node.currentUrl) {
|
||||
downloadURI(node.currentUrl, `Download Video Node-${node.kNode.id}`);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Error: Download Node Content",
|
||||
message: "This item does not have content for download.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
uiEvents.toolbarNode.DELETE.onClick(() =>
|
||||
this.commandManager.deleteNodes(),
|
||||
);
|
||||
uiEvents.toolbarNode.MOVE_LAYER_DOWN.onClick(() =>
|
||||
this.commandManager.moveNodesDown(),
|
||||
);
|
||||
uiEvents.toolbarNode.MOVE_LAYER_UP.onClick(() =>
|
||||
this.commandManager.moveNodesUp(),
|
||||
);
|
||||
uiEvents.toolbarNode.REMOVE_BACKGROUND.onClick(() =>
|
||||
this.removeBackground(),
|
||||
);
|
||||
|
||||
// Listen to Toolbar Main
|
||||
uiEvents.toolbarMain.UNDO.onClick(() => {
|
||||
this.undoStackManager.undo();
|
||||
});
|
||||
uiEvents.toolbarMain.REDO.onClick(() => {
|
||||
this.undoStackManager.redo();
|
||||
//this.realTimeDrawEngine.render();
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
|
||||
//await this.realTimeDrawEngine.saveOutput(); // This works.
|
||||
await invoke("open_login_command");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.GENERATE.onClick(async (/*event*/) => {
|
||||
console.log("generate clicked");
|
||||
await this.realTimeDrawEngine.generateImage();
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.SELECT.onClick(() => {
|
||||
console.log("Toolbar Main >> Select");
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PAINT.onClick(() => {
|
||||
console.log("Toolbar Main >> Paint");
|
||||
this.setAppMode(AppModes.PAINT);
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.ERASER.onClick(() => {
|
||||
console.log("Toolbar Main >> Eraser");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PREVIEW.onClick(async () => {
|
||||
console.log("Toolbar Main >> Preview");
|
||||
this.setAppMode(AppModes.PREVIEW);
|
||||
await this.handlePreview();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
});
|
||||
uiEvents.toolbarMain.DOWNLOAD.onClick(async () => {
|
||||
console.log("Toolbar Main >> Render Download");
|
||||
try {
|
||||
this.setAppMode(AppModes.RENDERING);
|
||||
} catch (error) {
|
||||
// throw error to retry
|
||||
uiAccess.dialogError.show({
|
||||
title: "Generation Error",
|
||||
message: error?.toString() || "Unknown Error",
|
||||
});
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
}
|
||||
});
|
||||
// Listen to other toolbars
|
||||
// VideoExtraction Toolbar
|
||||
|
||||
// Listen to other requests coming from the UI
|
||||
uiEvents.onGetStagedImage((image) => {
|
||||
this.addImage(image);
|
||||
});
|
||||
|
||||
uiEvents.onAddTextToEngine((textdata) => {
|
||||
this.addText(textdata);
|
||||
});
|
||||
|
||||
let renderTimeout: NodeJS.Timeout;
|
||||
|
||||
uiEvents.promptEvents.onPromptStrengthChanged(async (strength) => {
|
||||
this.realTimeDrawEngine.currentStrength = strength / 100.0;
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
uiEvents.promptEvents.onPromptTextChanged(async (prompt) => {
|
||||
this.realTimeDrawEngine.currentPrompt = prompt;
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
uiEvents.onAddShapeToEngine((shapeData) => {
|
||||
switch (shapeData.shape) {
|
||||
case "circle":
|
||||
this.addShape(ShapeType.CIRCLE, 100);
|
||||
break;
|
||||
case "square":
|
||||
this.addShape(ShapeType.SQUARE, 100);
|
||||
break;
|
||||
case "triangle":
|
||||
this.addShape(ShapeType.TRIANGLE, 100);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.onBgColorChanged((data) => {
|
||||
this.realTimeDrawEngine.updateBackground(data);
|
||||
});
|
||||
|
||||
uiEvents.toolbarNode.color.onConfirmChanged((nodeColor) => {
|
||||
console.log("Color change event triggered with:", nodeColor);
|
||||
if (!nodeColor) {
|
||||
console.warn("nodeColor is undefined or null");
|
||||
return;
|
||||
}
|
||||
if (!nodeColor.kNodeId) {
|
||||
console.warn("nodeColor.kNodeId is undefined or null");
|
||||
return;
|
||||
}
|
||||
this.changeNodeColor(nodeColor);
|
||||
});
|
||||
uiEvents.toolbarMain.onPaintColorChanged((color) => {
|
||||
this.realTimeDrawEngine.paintColor = color;
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.onEraseBrushSizeChanged((size: number) => {
|
||||
this.realTimeDrawEngine.paintBrushSize = size;
|
||||
});
|
||||
}
|
||||
|
||||
removeBackground() {
|
||||
console.log("Remove background");
|
||||
|
||||
const selectedNodeSet = this.selectionManager.getSelectedNodes();
|
||||
|
||||
if (selectedNodeSet.size !== 1) {
|
||||
return; // Can only remove background from a single image at a time.
|
||||
}
|
||||
|
||||
const [selectedNode] = selectedNodeSet;
|
||||
|
||||
const removeBgInvocation = async (
|
||||
selectedNode: ImageNode,
|
||||
callback: (
|
||||
existingImageNode: ImageNode,
|
||||
createdImage: ImageBitmap,
|
||||
) => void,
|
||||
) => {
|
||||
const image: HTMLImageElement = selectedNode.kNode.image();
|
||||
const base64Bitmap = await EncodeImageBitmapToBase64(image);
|
||||
|
||||
const falRemoveBgRequest: FalBackgroundRemovalRequest = {
|
||||
base64_image: base64Bitmap,
|
||||
};
|
||||
|
||||
const response = await FalBackgroundRemoval(falRemoveBgRequest);
|
||||
|
||||
if (response.status !== "success") {
|
||||
console.error("Failed to remove background", response);
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Remove background success", response);
|
||||
|
||||
if ("payload" in response) {
|
||||
const base64BitmapResponse = response.payload?.base64_bytes;
|
||||
|
||||
const createdImage = await DecodeBase64ToImage(
|
||||
base64BitmapResponse as string,
|
||||
);
|
||||
|
||||
callback(selectedNode, createdImage);
|
||||
}
|
||||
};
|
||||
|
||||
const _promise = removeBgInvocation(
|
||||
selectedNode,
|
||||
(selectedNode: ImageNode, createdImage: ImageBitmap) => {
|
||||
if (!this.nodesManager.hasNode(selectedNode)) {
|
||||
return;
|
||||
}
|
||||
this.commandManager.deleteSpecificNodes([selectedNode]);
|
||||
this.addImageFromImageBitmap(createdImage);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
uiAccess.toolbarNode.changeButtonState(name, { disabled: true });
|
||||
}
|
||||
}
|
||||
|
||||
async enableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
await uiAccess.toolbarNode.changeButtonState(name, { disabled: false });
|
||||
}
|
||||
}
|
||||
|
||||
sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
// Print window dimensions for debugging
|
||||
console.log("Window dimensions:", {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
outerWidth: window.outerWidth,
|
||||
outerHeight: window.outerHeight,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(
|
||||
window.innerWidth,
|
||||
window.innerHeight,
|
||||
);
|
||||
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
height: this.boardCanvasRef.offsetHeight,
|
||||
},
|
||||
});
|
||||
this.uiLayer.draw();
|
||||
this.stage.width(this.boardCanvasRef.offsetWidth);
|
||||
this.stage.height(this.boardCanvasRef.offsetHeight);
|
||||
this.stage.draw(); // Redraw the canvas
|
||||
}
|
||||
|
||||
// Sandbox is quickly a way to test your idea.
|
||||
public async sandbox() {}
|
||||
|
||||
public onMessage(event: MessageEvent) {
|
||||
console.log("Message From Shared Worker");
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
public initializeStage(sceneToken?: string) {
|
||||
// load canvas that was originaly saved TODO Save manager for resharing.
|
||||
uiAccess.toolbarNode.hide();
|
||||
uiAccess.loadingBar.hide();
|
||||
// load the scene if there's a scenetoken
|
||||
if (sceneToken) {
|
||||
this.sceneManager.loadScene(sceneToken);
|
||||
}
|
||||
this.setupStage();
|
||||
// this.populateWithDebugItems();
|
||||
}
|
||||
public isInitialized() {
|
||||
return this.stage !== null;
|
||||
}
|
||||
|
||||
public async setupStage() {
|
||||
// Frame rate inicator
|
||||
// const textNode = new Konva.Text({
|
||||
// x: 10,
|
||||
// y: 80,
|
||||
// text: "",
|
||||
// fontSize: 18,
|
||||
// fontFamily: "Source Sans 3",
|
||||
// fill: "white",
|
||||
// });
|
||||
// const anim = new Konva.Animation((frame) => {
|
||||
// if (frame && import.meta.env.DEV) {
|
||||
// const timeDiff = frame.timeDiff;
|
||||
// const frameRate = frame.frameRate;
|
||||
// textNode.setText(
|
||||
// `FrameTime: ${timeDiff.toFixed(0)} ms\nFrameRate: ${frameRate.toFixed(0)} fps`,
|
||||
// );
|
||||
// }
|
||||
// }, this.mediaLayer);
|
||||
|
||||
// anim.start();
|
||||
//this.uiLayer.add(textNode);
|
||||
this.addKeyboardShortcuts();
|
||||
}
|
||||
|
||||
private changeNodeColor(nodeColor: NodeColor) {
|
||||
console.log("Changing node color:", nodeColor);
|
||||
// Find the node that is selected with the id
|
||||
const idSelector = "#" + nodeColor.kNodeId;
|
||||
const selectedNode = this.mediaLayer.findOne(idSelector);
|
||||
console.log("Selected node:", selectedNode);
|
||||
console.log("ID selector:", idSelector);
|
||||
console.log("Media layer:", this.mediaLayer);
|
||||
// TODO: Change the color of the node
|
||||
// Turns out shapes are image nodes... how do we change the image's fill??
|
||||
if (selectedNode) {
|
||||
// Get the shape node from the real time drawing engine's image nodes
|
||||
const shapeNode = this.realTimeDrawEngine.findImageNodeById(
|
||||
nodeColor.kNodeId,
|
||||
);
|
||||
if (shapeNode && shapeNode instanceof ShapeNode) {
|
||||
// Create new shape node with updated color
|
||||
const newShapeNode = new ShapeNode({
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: shapeNode.shapeType,
|
||||
size: shapeNode.kNode.size(),
|
||||
color: nodeColor.color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
newShapeNode.kNode.position(shapeNode.kNode.position());
|
||||
newShapeNode.kNode.zIndex(shapeNode.kNode.zIndex());
|
||||
|
||||
// Remove old node and add new one
|
||||
shapeNode.kNode.destroy();
|
||||
this.commandManager.createNode(newShapeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addText(textNodeData: TextNodeData) {
|
||||
const textNode = new TextNode({
|
||||
textNodeData: textNodeData,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
});
|
||||
this.commandManager.createNode(textNode);
|
||||
}
|
||||
|
||||
public addShape(type: ShapeType, size: number, color?: string) {
|
||||
const shapeNode = new ShapeNode({
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: type,
|
||||
size: { width: size, height: size },
|
||||
color: color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(shapeNode);
|
||||
}
|
||||
public addPreviewCopy(image: Image.Konva) {
|
||||
// Start of Selection
|
||||
const copyNode = new PreviewCopyNode({
|
||||
image: image,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//this.realTimeDrawEngine.render();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(copyNode);
|
||||
}
|
||||
public addImage(imageFile: File) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageFile: imageFile,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(imageNode);
|
||||
}
|
||||
|
||||
public addImageFromImageBitmap(imageBitmap: ImageBitmap) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageBitmap: imageBitmap,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(imageNode);
|
||||
}
|
||||
|
||||
public addPaintNode(
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) {
|
||||
const node = new PaintNode({
|
||||
canvasElement: canvas,
|
||||
lineBounds: lineBounds,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(node);
|
||||
}
|
||||
|
||||
// Events for Undo and Redo
|
||||
private addKeyboardShortcuts() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.ctrlKey && event.key === "z") {
|
||||
this.undoStackManager.undo();
|
||||
} else if (
|
||||
(event.ctrlKey && event.key === "y") ||
|
||||
(event.ctrlKey && event.shiftKey && event.key === "Z")
|
||||
) {
|
||||
this.undoStackManager.redo();
|
||||
} else if (event.key === "Delete") {
|
||||
this.commandManager.deleteNodes();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move to utilities?
|
||||
function downloadURI(uri: string, name: string) {
|
||||
const link = document.createElement("a");
|
||||
link.download = name;
|
||||
link.target = "_blank";
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { uiAccess } from "../signals";
|
||||
import { CommandManager, MatteBox } from "./EngineUtitlities";
|
||||
import { ImageNode } from "./Nodes";
|
||||
import { PaintNode } from "./Nodes/PaintNode";
|
||||
import {
|
||||
NodesManager,
|
||||
NodesTranslationEventDetails,
|
||||
NodeTransformationEventDetails,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
SelectionManagerEvents,
|
||||
SelectorSquare,
|
||||
} from "./NodesManagers";
|
||||
import { UndoStackManager } from "./UndoRedo";
|
||||
|
||||
import { ToolbarMainButtonNames } from "../components/features/ToolbarMain/enum";
|
||||
import { EditModes, VideoResolutions } from "./constants";
|
||||
|
||||
import { ToolbarNodeButtonNames } from "../components/features/ToolbarNode/enums";
|
||||
|
||||
|
||||
export interface RenderingOptions {
|
||||
artstyle: string;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
cinematic: boolean;
|
||||
enginePreProcessing: boolean;
|
||||
faceDetail: boolean;
|
||||
lipSync: boolean;
|
||||
upscale: boolean;
|
||||
styleStrength: number;
|
||||
}
|
||||
|
||||
export class EditEngine {
|
||||
private editMode: EditModes = EditModes.SELECT;
|
||||
private boardCanvasRef: HTMLDivElement;
|
||||
private stage: Konva.Stage;
|
||||
|
||||
private mediaLayer: Konva.Layer;
|
||||
private nodeIsolationLayer: Konva.Layer;
|
||||
private uiLayer: Konva.Layer;
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
private realTimeDrawEngine: RealTimeDrawEngine;
|
||||
private nodesManager: NodesManager;
|
||||
private nodeTransformer: NodeTransformer;
|
||||
private selectionManager: SelectionManager;
|
||||
private selectorSquare: SelectorSquare;
|
||||
private matteBox: MatteBox;
|
||||
|
||||
private undoStackManager: UndoStackManager;
|
||||
private commandManager: CommandManager;
|
||||
|
||||
public segmentationButtonCanBePressed: boolean = true;
|
||||
// signal reference
|
||||
constructor(boardCanvasRef: HTMLDivElement) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Engine Created");
|
||||
}
|
||||
this.editMode = EditModes.SELECT;
|
||||
|
||||
this.boardCanvasRef = boardCanvasRef;
|
||||
|
||||
this.stage = new Konva.Stage({
|
||||
container: this.boardCanvasRef,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
//this.bgLayer = new Konva.Layer();
|
||||
|
||||
this.mediaLayer = new Konva.Layer();
|
||||
|
||||
this.nodeIsolationLayer = new Konva.Layer();
|
||||
this.uiLayer = new Konva.Layer();
|
||||
|
||||
//this.stage.add(this.bgLayer);
|
||||
this.stage.add(this.mediaLayer);
|
||||
this.stage.add(this.nodeIsolationLayer);
|
||||
this.stage.add(this.uiLayer);
|
||||
|
||||
// Konva Transformer
|
||||
this.nodeTransformer = new NodeTransformer();
|
||||
this.uiLayer.add(this.nodeTransformer.getKonvaNode());
|
||||
|
||||
// core layer for all the work done.
|
||||
this.offScreenCanvas = new OffscreenCanvas(0, 0);
|
||||
|
||||
// Collection of all Nodes
|
||||
this.nodesManager = new NodesManager();
|
||||
// Partial Collection of selected Nodes
|
||||
this.selectionManager = new SelectionManager({
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine = new RealTimeDrawEngine({
|
||||
width: VideoResolutions.SQUARE_1024.width,
|
||||
height: VideoResolutions.SQUARE_1024.height,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
offScreenCanvas: this.offScreenCanvas,
|
||||
onDraw: async (canvas, lineBounds) => {
|
||||
await this.addPaintNode(canvas, lineBounds);
|
||||
},
|
||||
});
|
||||
|
||||
// Selector Square to select Nodes
|
||||
this.selectorSquare = new SelectorSquare({
|
||||
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
stageRef: this.stage,
|
||||
});
|
||||
this.uiLayer.add(this.selectorSquare.getKonvaNode());
|
||||
|
||||
//Collection of commands for undo-redo
|
||||
this.undoStackManager = new UndoStackManager(() => {
|
||||
this.realTimeDrawEngine.render();
|
||||
});
|
||||
this.commandManager = new CommandManager({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
renderEngineRef: this.realTimeDrawEngine,
|
||||
undoStackManagerRef: this.undoStackManager,
|
||||
});
|
||||
|
||||
this.matteBox = new MatteBox({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.clientWidth,
|
||||
height: this.boardCanvasRef.clientHeight,
|
||||
},
|
||||
captureCanvasSize: this.realTimeDrawEngine.captureCanvas.getSize(),
|
||||
uiLayerRef: this.uiLayer,
|
||||
});
|
||||
|
||||
// some of the managers has events
|
||||
// hence, lastly, setup these events
|
||||
this.setupEventSystem();
|
||||
this.setEditMode(this.editMode);
|
||||
}
|
||||
|
||||
private setEditMode(newEditMode: EditModes) {
|
||||
this.editMode = newEditMode;
|
||||
switch (this.editMode) {
|
||||
case EditModes.SELECT:
|
||||
default: {
|
||||
console.log("EDITMODE: SELECT");
|
||||
this.realTimeDrawEngine.disablePaintMode();
|
||||
|
||||
this.selectorSquare.enable();
|
||||
this.selectionManager.enable();
|
||||
uiAccess.toolbarMain.enable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: true,
|
||||
});
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
|
||||
active: false,
|
||||
});
|
||||
this.matteBox.disable();
|
||||
document.body.style.cursor = "default";
|
||||
return;
|
||||
}
|
||||
case EditModes.EDIT: {
|
||||
console.log("EDITMODE: EDIT");
|
||||
this.realTimeDrawEngine.enablePaintMode();
|
||||
this.selectorSquare.disable();
|
||||
this.selectionManager.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
private setupEventSystem() {
|
||||
// Listen to changes in container size
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.onBoardCanvasResize();
|
||||
});
|
||||
resizeObserver.observe(this.boardCanvasRef);
|
||||
this.onBoardCanvasResize();
|
||||
|
||||
// Listen to Nodes
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSLATIONS,
|
||||
((event: CustomEvent<NodesTranslationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.translateNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSFORMATION,
|
||||
((event: CustomEvent<NodeTransformationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.transformNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
|
||||
// Listen to Toolbar Main
|
||||
// uiEvents.toolbarMain.UNDO.onClick(() => {
|
||||
// this.undoStackManager.undo();
|
||||
// });
|
||||
// uiEvents.toolbarMain.REDO.onClick(() => {
|
||||
// this.undoStackManager.redo();
|
||||
// this.realTimeDrawEngine.render();
|
||||
// });
|
||||
|
||||
// uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
|
||||
// await this.realTimeDrawEngine.saveOutput();
|
||||
// });
|
||||
|
||||
// uiEvents.toolbarMain.SELECT.onClick(() => {
|
||||
// console.log("Toolbar Main >> Select");
|
||||
// this.setAppMode(AppModes.SELECT);
|
||||
// });
|
||||
|
||||
// uiEvents.toolbarMain.PAINT.onClick(() => {
|
||||
// console.log("Toolbar Main >> Paint");
|
||||
// this.setAppMode(AppModes.PAINT);
|
||||
// });
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
uiAccess.toolbarNode.changeButtonState(name, { disabled: true });
|
||||
}
|
||||
}
|
||||
|
||||
async enableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
await uiAccess.toolbarNode.changeButtonState(name, { disabled: false });
|
||||
}
|
||||
}
|
||||
|
||||
sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(0, 0);
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
console.log("Board Canvas values below");
|
||||
console.log(this.boardCanvasRef.offsetWidth);
|
||||
console.log(this.boardCanvasRef.offsetHeight);
|
||||
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
height: this.boardCanvasRef.offsetHeight,
|
||||
},
|
||||
});
|
||||
|
||||
this.uiLayer.draw();
|
||||
this.stage.width(this.boardCanvasRef.offsetWidth);
|
||||
this.stage.height(this.boardCanvasRef.offsetHeight);
|
||||
this.stage.draw(); // Redraw the canvas
|
||||
}
|
||||
|
||||
// Sandbox is quickly a way to test your idea.
|
||||
public async sandbox() {}
|
||||
|
||||
public onMessage(event: MessageEvent) {
|
||||
console.log("Message From Shared Worker");
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
public initializeStage() {
|
||||
// load canvas that was originaly saved TODO Save manager for resharing.
|
||||
uiAccess.toolbarNode.hide();
|
||||
uiAccess.loadingBar.hide();
|
||||
this.setupStage();
|
||||
}
|
||||
|
||||
public isInitialized() {
|
||||
return this.stage !== null;
|
||||
}
|
||||
|
||||
public async setupStage() {
|
||||
// Frame rate inicator
|
||||
// const textNode = new Konva.Text({
|
||||
// x: 10,
|
||||
// y: 80,
|
||||
// text: "",
|
||||
// fontSize: 18,
|
||||
// fontFamily: "Source Sans 3",
|
||||
// fill: "white",
|
||||
// });
|
||||
// const anim = new Konva.Animation((frame) => {
|
||||
// if (frame && import.meta.env.DEV) {
|
||||
// const timeDiff = frame.timeDiff;
|
||||
// const frameRate = frame.frameRate;
|
||||
// textNode.setText(
|
||||
// `FrameTime: ${timeDiff.toFixed(0)} ms\nFrameRate: ${frameRate.toFixed(0)} fps`,
|
||||
// );
|
||||
// }
|
||||
// }, this.mediaLayer);
|
||||
|
||||
// anim.start();
|
||||
// this.uiLayer.add(textNode);
|
||||
this.addKeyboardShortcuts();
|
||||
}
|
||||
|
||||
public addImage(imageFile: File) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageFile: imageFile,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
this.setEditMode(EditModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(imageNode);
|
||||
}
|
||||
|
||||
public addImageFromImageBitmap(imageBitmap: ImageBitmap) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageBitmap: imageBitmap,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
this.setEditMode(EditModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(imageNode);
|
||||
}
|
||||
|
||||
public addPaintNode(
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) {
|
||||
var node = new PaintNode({
|
||||
canvasElement: canvas,
|
||||
lineBounds: lineBounds,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(node);
|
||||
}
|
||||
// Events for Undo and Redo
|
||||
private addKeyboardShortcuts() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.ctrlKey && event.key === "z") {
|
||||
this.undoStackManager.undo();
|
||||
} else if (
|
||||
(event.ctrlKey && event.key === "y") ||
|
||||
(event.ctrlKey && event.shiftKey && event.key === "Z")
|
||||
) {
|
||||
this.undoStackManager.redo();
|
||||
} else if (event.key === "Delete") {
|
||||
this.commandManager.deleteNodes();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
const ensureBase64Prefix = (base64String: string) => {
|
||||
return base64String.startsWith("data:")
|
||||
? base64String
|
||||
: `data:image/png;base64,${base64String}`;
|
||||
}
|
||||
|
||||
export { ensureBase64Prefix };
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { MediaNode, Position, RGBColor, Transformation } from "../types";
|
||||
import {
|
||||
|
||||
CreateCommand,
|
||||
DeleteCommand,
|
||||
LockNodesCommand,
|
||||
MoveLayerDown,
|
||||
MoveLayerUp,
|
||||
|
||||
TransformCommand,
|
||||
TranslateCommand,
|
||||
UndoStackManager,
|
||||
UnlockNodesCommand,
|
||||
|
||||
} from "../UndoRedo";
|
||||
import {
|
||||
NodesManager,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
} from "../NodesManagers";
|
||||
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
|
||||
interface EngineReferences {
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
renderEngineRef: RealTimeDrawEngine;
|
||||
undoStackManagerRef: UndoStackManager;
|
||||
}
|
||||
|
||||
export class CommandManager {
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodesManagerRef: NodesManager;
|
||||
private nodeTransformerRef: NodeTransformer;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
private renderEngineRef: RealTimeDrawEngine;
|
||||
private undoStackManagerRef: UndoStackManager;
|
||||
|
||||
constructor(engineRefs: EngineReferences) {
|
||||
this.mediaLayerRef = engineRefs.mediaLayerRef;
|
||||
this.nodesManagerRef = engineRefs.nodesManagerRef;
|
||||
this.nodeTransformerRef = engineRefs.nodeTransformerRef;
|
||||
this.selectionManagerRef = engineRefs.selectionManagerRef;
|
||||
this.renderEngineRef = engineRefs.renderEngineRef;
|
||||
this.undoStackManagerRef = engineRefs.undoStackManagerRef;
|
||||
}
|
||||
|
||||
createNode(node: MediaNode) {
|
||||
const command = new CreateCommand({
|
||||
nodes: new Set<MediaNode>([node]),
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
nodeTransformerRef: this.nodeTransformerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
|
||||
// Set the kNode's manual id to same as kNode's internal _id
|
||||
// This helps identify the kNode in the Konva layer with find
|
||||
node.kNode.id(node.kNode._id.toString());
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
deleteNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
const command = new DeleteCommand({
|
||||
nodes: nodes,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
nodeTransformerRef: this.nodeTransformerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
|
||||
deleteSpecificNodes(nodes: MediaNode[]) {
|
||||
const command = new DeleteCommand({
|
||||
nodes: nodes,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
nodeTransformerRef: this.nodeTransformerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
|
||||
toggleLockNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
const node = nodes.values().next().value;
|
||||
if (!node) {
|
||||
console.log("Node Not Found for Locking");
|
||||
return;
|
||||
}
|
||||
if (node.isLocked()) {
|
||||
const command = new UnlockNodesCommand({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
} else {
|
||||
const command = new LockNodesCommand({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
}
|
||||
}
|
||||
moveNodesUp() {
|
||||
const command = new MoveLayerUp({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
moveNodesDown() {
|
||||
const command = new MoveLayerDown({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
translateNodes(props: {
|
||||
nodes: Set<MediaNode>;
|
||||
initialPositions: Map<MediaNode, Position>;
|
||||
finalPositions: Map<MediaNode, Position>;
|
||||
}) {
|
||||
const command = new TranslateCommand({
|
||||
...props,
|
||||
layerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.pushCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
transformNodes(props: {
|
||||
nodes: Set<MediaNode>;
|
||||
initialTransformations: Map<MediaNode, Transformation[]>;
|
||||
finalTransformations: Map<MediaNode, Transformation[]>;
|
||||
}) {
|
||||
const command = new TransformCommand({
|
||||
...props,
|
||||
layerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.pushCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { Size } from "../types";
|
||||
import { Colors } from "../constants";
|
||||
import { uiAccess } from "../../signals/uiAccess";
|
||||
|
||||
export class MatteBox {
|
||||
private matteBoxPoly: Konva.Line;
|
||||
private lightBoxRect: Konva.Rect;
|
||||
private uiLayerRef: Konva.Layer;
|
||||
private boardCanvasSize: Size;
|
||||
private captureCanvasSize: Size;
|
||||
|
||||
constructor({
|
||||
boardCanvasSize,
|
||||
captureCanvasSize,
|
||||
uiLayerRef,
|
||||
}: {
|
||||
boardCanvasSize: Size;
|
||||
captureCanvasSize: Size;
|
||||
uiLayerRef: Konva.Layer;
|
||||
}) {
|
||||
this.uiLayerRef = uiLayerRef;
|
||||
this.boardCanvasSize = boardCanvasSize;
|
||||
this.captureCanvasSize = captureCanvasSize;
|
||||
this.matteBoxPoly = new Konva.Line({
|
||||
points: getMatteBoxPolygonPoints({
|
||||
boardCanvasSize: this.boardCanvasSize,
|
||||
captureCanvasSize: this.captureCanvasSize,
|
||||
}),
|
||||
fill: Colors.translucentBlack,
|
||||
closed: true,
|
||||
});
|
||||
this.lightBoxRect = new Konva.Rect({
|
||||
size: captureCanvasSize,
|
||||
position: {
|
||||
x: (boardCanvasSize.width - captureCanvasSize.width) / 2,
|
||||
y: (boardCanvasSize.height - captureCanvasSize.height) / 2,
|
||||
},
|
||||
fill: Colors.transparent,
|
||||
// fill: "rgba(100,0,0,0.3)",
|
||||
});
|
||||
}
|
||||
public enable(showMagicBox?: boolean) {
|
||||
this.uiLayerRef.add(this.matteBoxPoly);
|
||||
this.uiLayerRef.add(this.lightBoxRect);
|
||||
if (showMagicBox) {
|
||||
uiAccess.magicBox.show();
|
||||
}
|
||||
}
|
||||
public disable() {
|
||||
this.matteBoxPoly.remove();
|
||||
this.lightBoxRect.remove();
|
||||
if (uiAccess.magicBox.isShowing()) {
|
||||
uiAccess.magicBox.hide();
|
||||
}
|
||||
}
|
||||
public updateSize({
|
||||
boardCanvasSize,
|
||||
captureCanvasSize,
|
||||
}: {
|
||||
boardCanvasSize?: Size;
|
||||
captureCanvasSize?: Size;
|
||||
}) {
|
||||
if (boardCanvasSize) {
|
||||
this.boardCanvasSize = boardCanvasSize;
|
||||
}
|
||||
if (captureCanvasSize) {
|
||||
this.captureCanvasSize = captureCanvasSize;
|
||||
}
|
||||
this.matteBoxPoly.points(
|
||||
getMatteBoxPolygonPoints({
|
||||
boardCanvasSize: this.boardCanvasSize,
|
||||
captureCanvasSize: this.captureCanvasSize,
|
||||
}),
|
||||
);
|
||||
this.lightBoxRect.setAttrs({
|
||||
size: this.captureCanvasSize,
|
||||
position: {
|
||||
x: (this.boardCanvasSize.width - this.captureCanvasSize.width) / 2,
|
||||
y: (this.boardCanvasSize.height - this.captureCanvasSize.height) / 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getMatteBoxPolygonPoints({
|
||||
boardCanvasSize: board,
|
||||
captureCanvasSize: capture,
|
||||
}: {
|
||||
boardCanvasSize: Size;
|
||||
captureCanvasSize: Size;
|
||||
}) {
|
||||
return [
|
||||
//top left
|
||||
0,
|
||||
0,
|
||||
// top right
|
||||
board.width,
|
||||
0,
|
||||
//bottom right
|
||||
board.width,
|
||||
board.height,
|
||||
//bottom middle
|
||||
board.width / 2,
|
||||
board.height,
|
||||
//canvas bottom middle,
|
||||
board.width / 2,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//canvas bottom right,
|
||||
(board.width - capture.width) / 2 + capture.width,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//canvas top right,
|
||||
(board.width - capture.width) / 2 + capture.width,
|
||||
(board.height - capture.height) / 2,
|
||||
//canvas top left,
|
||||
(board.width - capture.width) / 2,
|
||||
(board.height - capture.height) / 2,
|
||||
//canvas bottom left,
|
||||
(board.width - capture.width) / 2,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//canvas bottom middle,
|
||||
board.width / 2,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//bottom middle
|
||||
board.width / 2,
|
||||
board.height,
|
||||
//bottom left
|
||||
0,
|
||||
board.height,
|
||||
//back to top left
|
||||
0,
|
||||
0,
|
||||
];
|
||||
}
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// TODO: USE API STORTELLE
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { NodesManager, SelectionManager } from "../NodesManagers";
|
||||
import {
|
||||
ImageNodeData,
|
||||
NodeData,
|
||||
TextNodeData,
|
||||
TransformationData,
|
||||
VideoNodeData,
|
||||
} from "../types";
|
||||
import { uiAccess } from "../../signals/uiAccess";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
import { NodeType } from "../Nodes/constants";
|
||||
import { ImageNode, TextNode } from "../Nodes";
|
||||
import { RealTimeDrawEngine as RenderEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
|
||||
|
||||
export class SceneManager {
|
||||
private navigateRef: NavigateFunction;
|
||||
|
||||
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodesManagerRef: NodesManager;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
private renderEngineRef: RenderEngine;
|
||||
private currentSceneToken: string | undefined;
|
||||
|
||||
constructor({
|
||||
navigateRef,
|
||||
|
||||
mediaLayerRef,
|
||||
nodesManagerRef,
|
||||
selectionManagerRef,
|
||||
renderEngineRef,
|
||||
}: {
|
||||
navigateRef: NavigateFunction;
|
||||
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
selectionManagerRef: SelectionManager;
|
||||
renderEngineRef: RenderEngine;
|
||||
}) {
|
||||
this.navigateRef = navigateRef;
|
||||
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodesManagerRef = nodesManagerRef;
|
||||
this.selectionManagerRef = selectionManagerRef;
|
||||
this.renderEngineRef = renderEngineRef;
|
||||
}
|
||||
|
||||
public async saveScene() {
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const timestamp = new Date();
|
||||
const sceneTitle =
|
||||
"board_" + timestamp.toISOString().substring(0, 19).replace(/\D/g, "");
|
||||
|
||||
const saveJson = this.extractAllNodesData();
|
||||
console.log("saveJson", saveJson);
|
||||
const file = new File([saveJson], `${sceneTitle}`, {
|
||||
type: "application/json",
|
||||
});
|
||||
if (!this.currentSceneToken) {
|
||||
const uploadResponse = await mediaUploadApi.UploadNewScene({
|
||||
blob: file,
|
||||
fileName: file.name,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
console.log(uploadResponse);
|
||||
if (!uploadResponse.success || !uploadResponse.data) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Save New Scene Error",
|
||||
message: uploadResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.currentSceneToken = uploadResponse.data;
|
||||
this.navigateRef(`/${this.currentSceneToken}`);
|
||||
} else {
|
||||
const uploadResponse = await mediaUploadApi.UploadSavedScene({
|
||||
blob: file,
|
||||
fileName: file.name,
|
||||
uuid: uuidv4(),
|
||||
mediaToken: this.currentSceneToken,
|
||||
});
|
||||
console.log(uploadResponse);
|
||||
if (!uploadResponse.success || !uploadResponse.data) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Save Scene Error",
|
||||
message: uploadResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async loadScene(mediaFileToken: string) {
|
||||
const mediaFileApi = new MediaFilesApi();
|
||||
const getSceneResponse = await mediaFileApi.GetMediaFileByToken({
|
||||
mediaFileToken: mediaFileToken,
|
||||
});
|
||||
if (!getSceneResponse.success || !getSceneResponse.data) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Load Scene Error",
|
||||
message: getSceneResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.currentSceneToken = mediaFileToken;
|
||||
// console.log(getSceneResponse.data);
|
||||
// console.log(getSceneResponse.data.public_bucket_path);
|
||||
const fileResponse = await fetch(getSceneResponse.data.public_bucket_url);
|
||||
if (!fileResponse.ok) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Load Scene Error",
|
||||
message: "Failed to download Scene File",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const fileBlob = await fileResponse.blob();
|
||||
|
||||
const loadJson: NodeData[] = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
const readerError = () => {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Load Scene Error",
|
||||
message: "Failed to read Scene File",
|
||||
});
|
||||
this.navigateRef("/");
|
||||
console.log("debug file Response", fileResponse);
|
||||
console.log("debug file Blob", fileBlob);
|
||||
reject();
|
||||
};
|
||||
reader.onloadend = () => {
|
||||
try {
|
||||
resolve(JSON.parse(reader.result as string));
|
||||
} catch {
|
||||
readerError();
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
readerError();
|
||||
};
|
||||
reader.readAsText(fileBlob);
|
||||
});
|
||||
|
||||
this.rebuildScene(loadJson);
|
||||
}
|
||||
|
||||
private extractAllNodesData() {
|
||||
const nodesData: NodeData[] = [];
|
||||
this.nodesManagerRef.getAllNodes().forEach((node) => {
|
||||
const nodeData = node.getNodeData(
|
||||
this.renderEngineRef.captureCanvas.position(),
|
||||
);
|
||||
if (nodeData !== null) {
|
||||
nodesData.push(nodeData);
|
||||
}
|
||||
});
|
||||
nodesData.sort((a, b) => a.transform.zIndex - b.transform.zIndex);
|
||||
return JSON.stringify(nodesData);
|
||||
}
|
||||
|
||||
private rebuildScene(nodesData: NodeData[]) {
|
||||
// console.log(nodesData);
|
||||
nodesData.forEach((nodeDatum) => {
|
||||
if (nodeDatum.type === NodeType.IMAGE && nodeDatum.imageNodeData) {
|
||||
this.addImage(nodeDatum.imageNodeData, nodeDatum.transform);
|
||||
} else if (nodeDatum.type === NodeType.VIDEO && nodeDatum.videoNodeData) {
|
||||
this.addVideo(nodeDatum.videoNodeData, nodeDatum.transform);
|
||||
} else if (nodeDatum.type === NodeType.TEXT && nodeDatum.textNodeData) {
|
||||
this.addText({
|
||||
textNodeData: nodeDatum.textNodeData,
|
||||
transform: nodeDatum.transform,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addImage(
|
||||
imageNodeData: ImageNodeData,
|
||||
transform: TransformationData,
|
||||
) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaFileUrl: imageNodeData.mediaFileUrl,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
canvasPosition: this.renderEngineRef.captureCanvas.position(),
|
||||
canvasSize: this.renderEngineRef.captureCanvas.size(),
|
||||
transform: transform,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
});
|
||||
this.nodesManagerRef.saveNode(imageNode);
|
||||
this.renderEngineRef.addNodes(imageNode);
|
||||
}
|
||||
private addVideo(
|
||||
videoNodeData: VideoNodeData,
|
||||
transform: TransformationData,
|
||||
) {
|
||||
const videoNode = new VideoNode({
|
||||
loadingVideosProviderRef: this.loadingVideosProviderRef,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
canvasPosition: this.renderEngineRef.captureCanvas.position(),
|
||||
canvasSize: this.renderEngineRef.captureCanvas.size(),
|
||||
videoNodeData: videoNodeData,
|
||||
transform: transform,
|
||||
});
|
||||
this.nodesManagerRef.saveNode(videoNode);
|
||||
this.renderEngineRef.addNodes(videoNode);
|
||||
}
|
||||
private addText({
|
||||
textNodeData,
|
||||
transform,
|
||||
}: {
|
||||
textNodeData: TextNodeData;
|
||||
transform: TransformationData;
|
||||
}) {
|
||||
const textNode = new TextNode({
|
||||
textNodeData: textNodeData,
|
||||
transform: transform,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
canvasPosition: this.renderEngineRef.captureCanvas.position(),
|
||||
canvasSize: this.renderEngineRef.captureCanvas.size(),
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
});
|
||||
this.nodesManagerRef.saveNode(textNode);
|
||||
this.renderEngineRef.addNodes(textNode);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CommandManager } from "./CommandManager";
|
||||
export { MatteBox } from "./MatteBox";
|
||||
export { SceneManager } from "./SceneManager";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user