Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab2f8f0ee | ||
|
|
ff360f83b1 | ||
|
|
fb68554f85 | ||
|
|
77fbd84006 | ||
|
|
bd3668ba5c | ||
|
|
8ae2cfa5fc | ||
|
|
c34f81975e | ||
|
|
b4a9da50de | ||
|
|
48a6b1a046 | ||
|
|
7791b9b6cc | ||
|
|
ff30213af8 | ||
|
|
766f726dc1 | ||
|
|
76d7d4c100 | ||
|
|
2902db074e | ||
|
|
b8ee03f1b7 | ||
|
|
2cf300588e | ||
|
|
d2720d0a62 | ||
|
|
69ed3ec48b | ||
|
|
81f5797e2f | ||
|
|
a80c69ab1e | ||
|
|
da859fc2f8 | ||
|
|
5f832d5d17 | ||
|
|
612e58483c | ||
|
|
11b07b5833 |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0848cd9ddf8f61f2dd4d57f0c58b66a60a985acaa1ee5753f30f5e6e107e080e"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = 'cancelled_by_system',\n retry_at = NULL,\n internal_debugging_failure_reason = 'reap_stale_fakeyou_jobs'\nWHERE\n job_type != 'fal_queue'\n AND status IN (\n 'pending',\n 'started',\n 'attempt_failed'\n )\n AND created_at > NOW() - INTERVAL 30 MINUTE\nLIMIT 5000\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "19708db99f2b3c6283d2115a511e0eb1c99fab396ad0effd9eb2dd79926e9c93"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "22b91a21cbb88abc9bc17b338cf2f9418e08e414667d44476623eb141ab17b87"
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- Migration history:
|
||||
-- tasks_v1.sqlite - initial version
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
|
||||
CREATE TABLE tasks (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
@@ -6,6 +9,8 @@ CREATE TABLE tasks (
|
||||
task_status TEXT NOT NULL,
|
||||
task_type TEXT NOT NULL,
|
||||
|
||||
model_type TEXT,
|
||||
|
||||
provider TEXT,
|
||||
provider_job_id TEXT,
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_model_type;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_model_type VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_generation_provider;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_generation_provider VARCHAR(16) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_anonymous_visitor_token;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_anonymous_visitor_token VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE prompt_context_items;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- An n:m join table between prompts and media_files.
|
||||
-- This associates mostly image media files with prompts, though we could conceivably
|
||||
-- also store 3D sets, etc.
|
||||
CREATE TABLE prompt_context_items (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The semantic type of context.
|
||||
context_semantic_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Foreign key, many-to-many
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
prompt_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Foreign key to the media token
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
media_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_prompt_token (prompt_token),
|
||||
KEY index_media_token (media_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs DROP COLUMN maybe_prompt_token;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
ADD COLUMN maybe_prompt_token VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files DROP COLUMN maybe_generation_provider;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
ADD COLUMN maybe_generation_provider VARCHAR(16) DEFAULT NULL;
|
||||
@@ -21,6 +21,8 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- The new enum for the type of job, which will eventually replace `inference_category` and `maybe_model_type`.
|
||||
-- This isn't fully supported yet in the inference-job, but we'll start populating it and adding logic around it.
|
||||
-- We'll start this out as nullable, then migrate the old rows with a default column value.
|
||||
--
|
||||
-- Fal jobs have the type `fal_queue`.
|
||||
job_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- There is an index on this column.
|
||||
@@ -103,6 +105,11 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- And furthermore, not all download URLs have a cover image.
|
||||
maybe_cover_image_media_file_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- If we pre-constructed a prompt, this will be copied to the resultant media generated.
|
||||
-- This is especially important for Fal-related jobs and endpoints. We need this for keeping
|
||||
-- track of all the prompt details, context images, etc.
|
||||
maybe_prompt_token VARCHAR(30) DEFAULT NULL,
|
||||
|
||||
-- A migration flag we can control from the backend API service which will tell
|
||||
-- the worker where to store the results. If false, GCP. If true, AWS.
|
||||
store_in_aws BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
@@ -61,6 +61,9 @@ CREATE TABLE media_files (
|
||||
-- * 'workflow' (DEPRECATED)
|
||||
origin_product_category VARCHAR(16) NOT NULL DEFAULT "unknown",
|
||||
|
||||
-- What service provided the generation (if any).
|
||||
maybe_generation_provider VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- For inference that can be tied back to a model, the type of model.
|
||||
-- There are multiple indices on this column!
|
||||
-- DO NOT EXPOSE THIS TO USERS VIA THE API, as we may leak secrets in doing so.
|
||||
|
||||
@@ -15,9 +15,9 @@ CREATE TABLE batch_generations (
|
||||
|
||||
-- The type of record this is attached to
|
||||
-- Currently supported values:
|
||||
-- * 'model_weight'
|
||||
-- * 'model_weight' (deprecated)
|
||||
-- * 'comment' (deprecated)
|
||||
-- * 'media_file'
|
||||
-- * 'comment'
|
||||
entity_type VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The token for the record.
|
||||
@@ -37,6 +37,4 @@ CREATE TABLE batch_generations (
|
||||
-- NB: Realistically, we probably don't need this index until we have a lot of entity types, and the entity_token
|
||||
-- entropy is probably sufficient enough as-is.
|
||||
-- KEY index_entity_type_and_token (entity_type, entity_token),
|
||||
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- An n:m join table between prompts and media_files.
|
||||
-- This associates mostly image media files with prompts, though we could conceivably
|
||||
-- also store 3D sets, etc.
|
||||
CREATE TABLE prompt_context_items (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The semantic type of context.
|
||||
context_semantic_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Foreign key, many-to-many
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
prompt_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Foreign key to the media token
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
media_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_prompt_token (prompt_token),
|
||||
KEY index_media_token (media_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -19,13 +19,25 @@ CREATE TABLE prompts (
|
||||
-- Original product to which the prompt applied.
|
||||
-- Prompts may or may not be compatible across products.
|
||||
-- Possible values:
|
||||
-- * 'stable_diffusion'
|
||||
-- * 'comfy_ui'
|
||||
-- * 'artcraft_app'
|
||||
-- * 'stable_diffusion' (legacy)
|
||||
-- * 'comfy_ui' (legacy)
|
||||
prompt_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Whomever created the prompt
|
||||
-- What type of model was used to generate the result.
|
||||
-- NB: We're conflating prompts with outputs, but in a
|
||||
-- sense the model is inextricably part of the prompt.
|
||||
maybe_model_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- What service provided the generation.
|
||||
maybe_generation_provider VARCHAR(16) NOT NULL, -- TODO
|
||||
|
||||
-- Whoever created the prompt
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Whoever created the prompt (anonymous user tracking)
|
||||
maybe_anonymous_visitor_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== PROMPT DETAILS ==========
|
||||
|
||||
-- The positive prompt (optional)
|
||||
@@ -37,10 +37,10 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo2Duration::Default => None,
|
||||
Veo2Duration::FiveSeconds => Some("5".to_string()),
|
||||
Veo2Duration::SixSeconds => Some("6".to_string()),
|
||||
Veo2Duration::SevenSeconds => Some("7".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8".to_string()),
|
||||
Veo2Duration::FiveSeconds => Some("5s".to_string()),
|
||||
Veo2Duration::SixSeconds => Some("6s".to_string()),
|
||||
Veo2Duration::SevenSeconds => Some("7s".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use sqlite_tasks::error::SqliteTasksError;
|
||||
@@ -34,10 +35,33 @@ impl TaskEnqueueSuccess{
|
||||
}
|
||||
|
||||
pub async fn insert_into_task_database(&self, task_database: &TaskDatabase) -> Result<TaskId, SqliteTasksError> {
|
||||
// TODO: Move this mapping elsewhere, or remove the other models.
|
||||
let model_type = match self.model {
|
||||
None => None,
|
||||
Some(GenerationModel::Flux1Dev) => Some(TaskModelType::Flux1Dev),
|
||||
Some(GenerationModel::Flux1Schnell) => Some(TaskModelType::Flux1Schnell),
|
||||
Some(GenerationModel::FluxPro11) => Some(TaskModelType::FluxPro11),
|
||||
Some(GenerationModel::FluxPro11Ultra) => Some(TaskModelType::FluxPro11Ultra),
|
||||
Some(GenerationModel::GptImage1) => Some(TaskModelType::GptImage1),
|
||||
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
|
||||
Some(GenerationModel::Kling21Pro) => Some(TaskModelType::Kling21Pro),
|
||||
Some(GenerationModel::Kling21Master) => Some(TaskModelType::Kling21Master),
|
||||
Some(GenerationModel::Seedance10Lite) => Some(TaskModelType::Seedance10Lite),
|
||||
Some(GenerationModel::Veo2) => Some(TaskModelType::Veo2),
|
||||
Some(GenerationModel::Hunyuan3d2_0) => Some(TaskModelType::Hunyuan3d2_0),
|
||||
Some(GenerationModel::Hunyuan3d2_1) => Some(TaskModelType::Hunyuan3d2_1),
|
||||
|
||||
// TODO: These seem wrong -
|
||||
Some(GenerationModel::Kling1_6) => Some(TaskModelType::Kling16Pro), // NB: `VideoModel::Kling16Pro`.
|
||||
Some(GenerationModel::Kling2_0) => None, // TODO: unused elsewhere?
|
||||
Some(GenerationModel::Sora) => None, // TODO: unused elsewhere?
|
||||
};
|
||||
|
||||
create_task(CreateTaskArgs {
|
||||
db: task_database.get_connection(),
|
||||
status: TaskStatus::Pending,
|
||||
task_type: self.task_type,
|
||||
model_type: model_type,
|
||||
provider: self.provider,
|
||||
provider_job_id: self.provider_job_id.as_deref(),
|
||||
frontend_subscriber_id: None,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use futures::TryFutureExt;
|
||||
use log::{error, info};
|
||||
use reqwest::Client;
|
||||
use tauri::ipc::Response;
|
||||
|
||||
/// Request a URL and load it without worrying about CORS.
|
||||
#[tauri::command]
|
||||
pub async fn load_without_cors_command(url: String) -> Result<Response, String> {
|
||||
|
||||
info!("load_without_cors_command for URL: {}", url);
|
||||
|
||||
// TODO(bt,2025-07-22): Make sure the URL is valid and safe to fetch. Check an allowlist.
|
||||
|
||||
// TODO(bt,2025-07-22): Should we cache clients so we don't need to SSL handshake every time?
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
error!("Failed to create HTTP client: {:?}", e);
|
||||
format!("Failed to create HTTP client: {:?}", e)
|
||||
})?;
|
||||
|
||||
let response = client.get(url.clone())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to send HTTP request: {:?}", e);
|
||||
format!("Failed to send HTTP request: {:?}", e)
|
||||
})?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(format!("Failed to fetch URL: {}. Status: {}", url, response.status()));
|
||||
}
|
||||
|
||||
let bytes = response.bytes()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to read response bytes: {:?}", e);
|
||||
format!("Failed to read response bytes: {:?}", e)
|
||||
})?;
|
||||
|
||||
Ok(Response::new(bytes.to_vec()))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub mod app_preferences;
|
||||
pub mod enqueue;
|
||||
pub mod flip_image;
|
||||
pub mod get_app_info_command;
|
||||
pub mod load_without_cors_command;
|
||||
pub mod platform_info_command;
|
||||
pub mod providers;
|
||||
pub mod response;
|
||||
@@ -25,8 +25,11 @@ pub enum GenerationModel {
|
||||
Hunyuan3d2_0,
|
||||
#[serde(rename = "hunyuan_3d_2_1")]
|
||||
Hunyuan3d2_1,
|
||||
|
||||
// TODO: Should be Kling16Pro
|
||||
#[serde(rename = "kling_1.6")]
|
||||
Kling1_6,
|
||||
|
||||
#[serde(rename = "kling_2.0")]
|
||||
Kling2_0,
|
||||
#[serde(rename = "kling_2.1_master")]
|
||||
|
||||
@@ -22,7 +22,7 @@ impl DataSubdir for AppStateDir{
|
||||
|
||||
impl AppStateDir {
|
||||
pub fn get_tasks_sqlite_database_path(&self) -> PathBuf {
|
||||
self.path.join("tasks_v1.sqlite")
|
||||
self.path.join("tasks_v2.sqlite")
|
||||
}
|
||||
|
||||
pub fn get_window_size_config_file(&self) -> PathBuf {
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::
|
||||
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
|
||||
use crate::core::commands::flip_image::flip_image;
|
||||
use crate::core::commands::get_app_info_command::get_app_info_command;
|
||||
use crate::core::commands::load_without_cors_command::load_without_cors_command;
|
||||
use crate::core::commands::platform_info_command::platform_info_command;
|
||||
use crate::core::commands::providers::get_provider_order_command::get_provider_order_command;
|
||||
use crate::core::commands::providers::set_provider_order_command::set_provider_order_command;
|
||||
@@ -154,6 +155,7 @@ pub fn run() {
|
||||
get_app_preferences_command,
|
||||
get_fal_api_key_command,
|
||||
get_provider_order_command,
|
||||
load_without_cors_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
set_fal_api_key_command,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use log::{debug, warn};
|
||||
use crate::payloads::generic_inference_args::inner_payloads::f5_tts_payload::F5TTSPayload;
|
||||
use crate::payloads::generic_inference_args::inner_payloads::face_fusion_payload::FaceFusionPayload;
|
||||
use crate::payloads::generic_inference_args::inner_payloads::gptsovits_payload::GptSovitsPayload;
|
||||
@@ -180,6 +181,19 @@ impl GenericInferenceArgs {
|
||||
Ok(serde_json::from_str(json)?)
|
||||
}
|
||||
|
||||
pub fn from_json_optional(json: Option<&str>) -> AnyhowResult<Option<Self>> {
|
||||
match json {
|
||||
None => Ok(None),
|
||||
Some("" | "null" | "NULL" | "undefined" | "UNDEFINED") => {
|
||||
// NB: Something inserted a few of these records (only in local dev).
|
||||
// It's better not to stall the job queue than to assert correctness.
|
||||
warn!("Received empty or null JSON `{json:?}` for GenericInferenceArgs, returning None.");
|
||||
Ok(None)
|
||||
},
|
||||
Some(json_str) => Ok(Some(Self::from_json(json_str)?)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> AnyhowResult<String> {
|
||||
Ok(serde_json::to_string(self)?)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::helpers::boolean_converters::i8_to_bool;
|
||||
@@ -30,11 +31,35 @@ pub struct FalJobDetails {
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: String,
|
||||
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RawJobRecord {
|
||||
job_token: InferenceJobToken,
|
||||
|
||||
status: JobStatusPlus,
|
||||
|
||||
// NB: Nullable, but required to be filled for this query
|
||||
external_third_party: Option<InferenceJobExternalThirdParty>,
|
||||
|
||||
// NB: Nullable, but required to be filled for this query
|
||||
external_third_party_id: Option<String>,
|
||||
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
creator_ip_address: String,
|
||||
|
||||
maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
created_at: DateTime<Utc>,
|
||||
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>>
|
||||
@@ -66,6 +91,8 @@ SELECT
|
||||
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.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
|
||||
|
||||
jobs.created_at,
|
||||
jobs.updated_at
|
||||
@@ -106,28 +133,8 @@ fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<FalJobDetai
|
||||
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,
|
||||
maybe_prompt_token: record.maybe_prompt_token,
|
||||
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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use sqlx::MySqlPool;
|
||||
use sqlx::{Executor, MySql};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
|
||||
@@ -13,6 +15,7 @@ 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::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::errors::database_query_error::DatabaseQueryError;
|
||||
@@ -27,26 +30,35 @@ pub enum FalCategory {
|
||||
ObjectGeneration,
|
||||
}
|
||||
|
||||
pub struct InsertGenericInferenceForFalArgs<'a> {
|
||||
pub uuid_idempotency_token: &'a str,
|
||||
pub struct InsertGenericInferenceForFalArgs<'e, 'c, E>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
pub uuid_idempotency_token: &'e str,
|
||||
|
||||
/// The external primary key identifier for the job.
|
||||
pub maybe_external_third_party_id: &'a str,
|
||||
pub maybe_external_third_party_id: &'e str,
|
||||
|
||||
pub fal_category: FalCategory,
|
||||
|
||||
pub maybe_inference_args: Option<GenericInferenceArgs>,
|
||||
|
||||
pub maybe_prompt_token: Option<&'e PromptToken>,
|
||||
|
||||
pub maybe_creator_user_token: Option<&'a UserToken>,
|
||||
pub maybe_avt_token: Option<&'a AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: &'a str,
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: &'e str,
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
pub mysql_executor: E,
|
||||
|
||||
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
|
||||
// See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub async fn insert_generic_inference_job_for_fal_queue(args: InsertGenericInferenceForFalArgs<'_>)
|
||||
pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: InsertGenericInferenceForFalArgs<'e, 'c, E>)
|
||||
-> Result<InferenceJobToken, DatabaseQueryError>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
let job_token = InferenceJobToken::generate();
|
||||
|
||||
@@ -105,6 +117,8 @@ SET
|
||||
|
||||
maybe_download_url = NULL,
|
||||
maybe_cover_image_media_file_token = NULL,
|
||||
|
||||
maybe_prompt_token = ?,
|
||||
|
||||
maybe_raw_inference_text = NULL,
|
||||
|
||||
@@ -134,6 +148,8 @@ SET
|
||||
|
||||
product_category.to_str(),
|
||||
inference_category.to_str(),
|
||||
|
||||
args.maybe_prompt_token.map(|t| t.to_string()),
|
||||
|
||||
serialized_args_payload,
|
||||
|
||||
@@ -145,7 +161,7 @@ SET
|
||||
STATUS.to_str(),
|
||||
);
|
||||
|
||||
let query_result = query.execute(args.mysql_pool)
|
||||
let query_result = query.execute(args.mysql_executor)
|
||||
.await;
|
||||
|
||||
let record_id = match query_result {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use log::debug;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
@@ -151,10 +152,8 @@ pub async fn list_available_generic_inference_jobs(
|
||||
.map(|token_type| InferenceInputSourceTokenType::from_str(token_type)
|
||||
.map_err(|err| anyhow!("error: {:?}", err)))
|
||||
.transpose()?,
|
||||
maybe_inference_args: record.maybe_inference_args
|
||||
.as_deref()
|
||||
.map(|args| GenericInferenceArgs::from_json(args))
|
||||
.transpose()?,
|
||||
maybe_inference_args: GenericInferenceArgs::from_json_optional(
|
||||
record.maybe_inference_args.as_deref())?,
|
||||
maybe_raw_inference_text: record.maybe_raw_inference_text,
|
||||
status: JobStatusPlus::from_str(&record.status)
|
||||
.map_err(|err| anyhow!("JobStatus failure to parse: {:?}", err))?,
|
||||
|
||||
@@ -5,3 +5,4 @@ pub mod mark_generic_inference_job_completely_failed;
|
||||
pub mod mark_generic_inference_job_failure;
|
||||
pub mod mark_generic_inference_job_pending_and_grab_lock;
|
||||
pub mod mark_generic_inference_job_successfully_done;
|
||||
pub mod reap_stale_fakeyou_jobs;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use sqlx;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// Kill FakeYou jobs that are "stale".
|
||||
///
|
||||
/// - This leaves Fal jobs alone, as they do not use the job runner.
|
||||
/// - This kills jobs that are over a threshold (e.g. an hour) overdue.
|
||||
pub async fn reap_stale_fakeyou_jobs(mysql_pool: &MySqlPool) -> AnyhowResult<()>
|
||||
{
|
||||
let mut connection = mysql_pool.acquire().await?;
|
||||
reap_stale_fakeyou_jobs_using_connection(&mut connection).await
|
||||
}
|
||||
|
||||
pub async fn reap_stale_fakeyou_jobs_using_connection(
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<()> {
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE generic_inference_jobs
|
||||
SET
|
||||
status = 'cancelled_by_system',
|
||||
retry_at = NULL,
|
||||
internal_debugging_failure_reason = 'reap_stale_fakeyou_jobs'
|
||||
WHERE
|
||||
job_type != 'fal_queue'
|
||||
AND status IN (
|
||||
'pending',
|
||||
'started',
|
||||
'attempt_failed'
|
||||
)
|
||||
AND created_at > NOW() - INTERVAL 30 MINUTE
|
||||
LIMIT 5000
|
||||
"#
|
||||
)
|
||||
.execute(&mut **mysql_connection)
|
||||
.await;
|
||||
|
||||
match query_result {
|
||||
Ok(result) => {
|
||||
info!("Reaped stale FakeYou jobs: {} rows affected.", result.rows_affected());
|
||||
Ok(())
|
||||
},
|
||||
Err(err) => {
|
||||
Err(anyhow!("error with reaping stale jobs: {:?}", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
use crate::queries::media_files::create::generic_insert::insert_media_file_generic::{insert_media_file_generic, InsertArgs};
|
||||
use anyhow::anyhow;
|
||||
use crate::queries::media_files::create::insert_builder::media_file_insert_builder_error::MediaFileInsertBuilderError;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
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 enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::MySqlPool;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use crate::queries::media_files::create::insert_builder::media_file_insert_builder_error::MediaFileInsertBuilderError;
|
||||
|
||||
/// Builder for inserting a media files into the database.
|
||||
/// Resembles an ORM, but can still use statically type checked queries (which is probably
|
||||
@@ -53,7 +52,9 @@ pub struct MediaFileInsertBuilder {
|
||||
checksum_sha2: Option<String>, // NB: Non-nullable field
|
||||
|
||||
// Media info for certain product areas
|
||||
pub maybe_engine_category: Option<MediaFileEngineCategory>, // TODO: Deprecate
|
||||
maybe_engine_category: Option<MediaFileEngineCategory>, // TODO: Deprecate
|
||||
|
||||
maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
// // User text information
|
||||
// maybe_title: Option<&'a str>,
|
||||
@@ -100,6 +101,7 @@ impl MediaFileInsertBuilder {
|
||||
checksum_sha2: None,
|
||||
maybe_engine_category: None,
|
||||
public_bucket_directory_hash: None,
|
||||
maybe_prompt_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +167,11 @@ impl MediaFileInsertBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn maybe_prompt_token(mut self, maybe_prompt_token: Option<&PromptToken>) -> Self {
|
||||
self.maybe_prompt_token = maybe_prompt_token.map(|token| token.clone());
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: Method for maybe_origin_model_type
|
||||
// TODO: Method for maybe_origin_model_token
|
||||
// TODO: Method for maybe_origin_filename
|
||||
@@ -273,7 +280,7 @@ impl MediaFileInsertBuilder {
|
||||
maybe_title: None, // TODO
|
||||
maybe_text_transcript: None, // TODO
|
||||
maybe_scene_source_media_file_token: None, // TODO
|
||||
maybe_prompt_token: None, // TODO
|
||||
maybe_prompt_token: self.maybe_prompt_token.as_ref(),
|
||||
maybe_batch_token: None, // TODO
|
||||
public_bucket_directory_hash: bucket_path.get_object_hash(),
|
||||
maybe_public_bucket_prefix: bucket_path.get_optional_prefix(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::mysql::MySqlRow;
|
||||
use sqlx::{Acquire, FromRow, MySql, MySqlPool, QueryBuilder, Row};
|
||||
|
||||
use sqlx::pool::PoolConnection;
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
@@ -84,6 +84,18 @@ pub async fn batch_get_media_files_by_tokens(
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn batch_get_media_files_by_tokens_with_connection(
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
can_see_deleted: bool
|
||||
) -> AnyhowResult<Vec<MediaFilesByTokensRecord>> {
|
||||
batch_get_media_files_by_tokens_with_transactor(
|
||||
Transactor::Connection{ connection: mysql_connection },
|
||||
media_file_tokens,
|
||||
can_see_deleted
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn batch_get_media_files_by_tokens_with_transactor(
|
||||
transactor: Transactor<'_, '_>,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
@@ -16,6 +14,8 @@ use enums::by_table::model_weights::weights_category::WeightsCategory;
|
||||
use enums::by_table::model_weights::weights_types::WeightsType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -215,6 +215,18 @@ pub async fn get_media_file(
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn get_media_file_with_connection(
|
||||
media_file_token: &MediaFileToken,
|
||||
can_see_deleted: bool,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<Option<MediaFile>> {
|
||||
get_media_file_with_transactor(
|
||||
media_file_token,
|
||||
can_see_deleted,
|
||||
Transactor::for_connection(mysql_connection)
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn get_media_file_with_transactor(
|
||||
media_file_token: &MediaFileToken,
|
||||
can_see_deleted: bool,
|
||||
|
||||
@@ -6,6 +6,8 @@ use sqlx;
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::model_type::ModelType;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
@@ -21,6 +23,10 @@ pub struct InsertPromptArgs<'e, 'c, E>
|
||||
pub prompt_type: PromptType,
|
||||
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
|
||||
pub maybe_model_type: Option<ModelType>,
|
||||
|
||||
pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
|
||||
pub maybe_positive_prompt: Option<&'e str>,
|
||||
|
||||
@@ -65,9 +71,13 @@ SET
|
||||
prompt_type = ?,
|
||||
|
||||
maybe_creator_user_token = ?,
|
||||
|
||||
maybe_model_type = ?,
|
||||
maybe_generation_provider = ?,
|
||||
|
||||
maybe_positive_prompt = ?,
|
||||
maybe_negative_prompt = ?,
|
||||
|
||||
maybe_other_args = ?,
|
||||
|
||||
creator_ip_address = ?
|
||||
@@ -75,6 +85,8 @@ SET
|
||||
prompt_token.as_str(),
|
||||
args.prompt_type.to_str(),
|
||||
args.maybe_creator_user_token.map(|t| t.as_str()),
|
||||
args.maybe_model_type.map(|m| m.to_str()),
|
||||
args.maybe_generation_provider.map(|g| g.to_str()),
|
||||
args.maybe_positive_prompt,
|
||||
args.maybe_negative_prompt,
|
||||
maybe_other_args,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::connection::TaskDbConnection;
|
||||
use crate::error::SqliteTasksError;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
@@ -9,6 +10,7 @@ pub struct CreateTaskArgs<'a> {
|
||||
pub db: &'a TaskDbConnection,
|
||||
pub status: TaskStatus,
|
||||
pub task_type: TaskType,
|
||||
pub model_type: Option<TaskModelType>,
|
||||
pub provider: GenerationProvider,
|
||||
pub provider_job_id: Option<&'a str>,
|
||||
pub frontend_subscriber_id: Option<&'a str>,
|
||||
@@ -24,6 +26,7 @@ pub async fn create_task(
|
||||
let task_id_temp = task_id.as_str();
|
||||
let status_temp = args.status.to_str();
|
||||
let task_type_temp = args.task_type.to_str();
|
||||
let model_type_temp = args.model_type.map(|s| s.to_str());
|
||||
let provider_temp = args.provider.to_string();
|
||||
|
||||
let query = sqlx::query!(r#"
|
||||
@@ -31,16 +34,18 @@ pub async fn create_task(
|
||||
id,
|
||||
task_status,
|
||||
task_type,
|
||||
model_type,
|
||||
provider,
|
||||
provider_job_id,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
task_id_temp,
|
||||
status_temp,
|
||||
task_type_temp,
|
||||
model_type_temp,
|
||||
provider_temp,
|
||||
args.provider_job_id,
|
||||
args.frontend_subscriber_id,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::connection::TaskDbConnection;
|
||||
use crate::error::SqliteTasksError;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use sqlx::{QueryBuilder, Sqlite};
|
||||
@@ -21,6 +22,7 @@ pub struct Task {
|
||||
pub id: TaskId,
|
||||
pub status: TaskStatus,
|
||||
pub task_type: TaskType,
|
||||
pub model_type: TaskModelType,
|
||||
pub provider: GenerationProvider,
|
||||
pub provider_job_id: Option<String>,
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
@@ -33,6 +35,7 @@ struct RawTask {
|
||||
id: String,
|
||||
task_status: String,
|
||||
task_type: String,
|
||||
model_type: String,
|
||||
provider: String,
|
||||
provider_job_id: Option<String>,
|
||||
frontend_subscriber_id: Option<String>,
|
||||
@@ -48,6 +51,7 @@ pub async fn list_tasks_by_provider_and_tokens(
|
||||
id,
|
||||
task_status,
|
||||
task_type,
|
||||
model_type,
|
||||
provider,
|
||||
provider_job_id,
|
||||
frontend_subscriber_id,
|
||||
@@ -87,6 +91,8 @@ pub async fn list_tasks_by_provider_and_tokens(
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
task_type: TaskType::from_str(&task.task_type)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
model_type: TaskModelType::from_str(&task.model_type)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
provider: GenerationProvider::from_str(&task.provider)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
provider_job_id: task.provider_job_id,
|
||||
|
||||
@@ -259,7 +259,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 32;
|
||||
// NB: The media_files table has allocated width for VARCHAR(32), but let's slim it down to 24.
|
||||
const MAX_LENGTH : usize = 24;
|
||||
for variant in MediaFileOriginModelType::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod media_files;
|
||||
pub mod media_uploads;
|
||||
pub mod model_categories;
|
||||
pub mod model_weights;
|
||||
pub mod prompt_context_items;
|
||||
pub mod prompts;
|
||||
pub mod tag_uses;
|
||||
pub mod trending_model_analytics;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod prompt_context_semantic_type;
|
||||
@@ -0,0 +1,163 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Used in the `prompt_context_items` table in a `VARCHAR(16)` field.
|
||||
///
|
||||
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PromptContextSemanticType {
|
||||
/// Image-to-Video starting frame
|
||||
VidStartFrame,
|
||||
|
||||
/// Image-to-Video ending frame
|
||||
VidEndFrame,
|
||||
|
||||
/// Standard image reference without a semantic type (e.g. Sora/ChatGPT 4o/gpt-image-1)
|
||||
Imgref,
|
||||
|
||||
ImgrefCharacter,
|
||||
ImgrefStyle,
|
||||
ImgrefBg,
|
||||
}
|
||||
|
||||
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
|
||||
impl_enum_display_and_debug_using_to_str!(PromptContextSemanticType);
|
||||
impl_mysql_enum_coders!(PromptContextSemanticType);
|
||||
impl_mysql_from_row!(PromptContextSemanticType);
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl PromptContextSemanticType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::VidStartFrame => "vid_start_frame",
|
||||
Self::VidEndFrame => "vid_end_frame",
|
||||
Self::Imgref => "imgref",
|
||||
Self::ImgrefCharacter => "imgref_character",
|
||||
Self::ImgrefStyle => "imgref_style",
|
||||
Self::ImgrefBg => "imgref_bg",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"vid_start_frame" => Ok(Self::VidStartFrame),
|
||||
"vid_end_frame" => Ok(Self::VidEndFrame),
|
||||
"imgref" => Ok(Self::Imgref),
|
||||
"imgref_character" => Ok(Self::ImgrefCharacter),
|
||||
"imgref_style" => Ok(Self::ImgrefStyle),
|
||||
"imgref_bg" => Ok(Self::ImgrefBg),
|
||||
_ => 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::VidStartFrame,
|
||||
Self::VidEndFrame,
|
||||
Self::Imgref,
|
||||
Self::ImgrefCharacter,
|
||||
Self::ImgrefStyle,
|
||||
Self::ImgrefBg,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod serde {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(PromptContextSemanticType::VidStartFrame, "vid_start_frame");
|
||||
assert_serialization(PromptContextSemanticType::VidEndFrame, "vid_end_frame");
|
||||
assert_serialization(PromptContextSemanticType::Imgref, "imgref");
|
||||
assert_serialization(PromptContextSemanticType::ImgrefCharacter, "imgref_character");
|
||||
assert_serialization(PromptContextSemanticType::ImgrefStyle, "imgref_style");
|
||||
assert_serialization(PromptContextSemanticType::ImgrefBg, "imgref_bg");
|
||||
}
|
||||
}
|
||||
|
||||
mod impl_methods {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(PromptContextSemanticType::VidStartFrame.to_str(), "vid_start_frame");
|
||||
assert_eq!(PromptContextSemanticType::VidEndFrame.to_str(), "vid_end_frame");
|
||||
assert_eq!(PromptContextSemanticType::Imgref.to_str(), "imgref");
|
||||
assert_eq!(PromptContextSemanticType::ImgrefCharacter.to_str(), "imgref_character");
|
||||
assert_eq!(PromptContextSemanticType::ImgrefStyle.to_str(), "imgref_style");
|
||||
assert_eq!(PromptContextSemanticType::ImgrefBg.to_str(), "imgref_bg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(PromptContextSemanticType::from_str("vid_start_frame").unwrap(), PromptContextSemanticType::VidStartFrame);
|
||||
assert_eq!(PromptContextSemanticType::from_str("vid_end_frame").unwrap(), PromptContextSemanticType::VidEndFrame);
|
||||
assert_eq!(PromptContextSemanticType::from_str("imgref").unwrap(), PromptContextSemanticType::Imgref);
|
||||
assert_eq!(PromptContextSemanticType::from_str("imgref_character").unwrap(), PromptContextSemanticType::ImgrefCharacter);
|
||||
assert_eq!(PromptContextSemanticType::from_str("imgref_style").unwrap(), PromptContextSemanticType::ImgrefStyle);
|
||||
assert_eq!(PromptContextSemanticType::from_str("imgref_bg").unwrap(), PromptContextSemanticType::ImgrefBg);
|
||||
assert!(PromptContextSemanticType::from_str("foo").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod manual_variant_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = PromptContextSemanticType::all_variants();
|
||||
assert_eq!(variants.len(), 6);
|
||||
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::VidStartFrame));
|
||||
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::VidEndFrame));
|
||||
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::Imgref));
|
||||
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefCharacter));
|
||||
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefStyle));
|
||||
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefBg));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(PromptContextSemanticType::all_variants().len(), PromptContextSemanticType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in PromptContextSemanticType::all_variants() {
|
||||
assert_eq!(variant, PromptContextSemanticType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, PromptContextSemanticType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, PromptContextSemanticType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 16;
|
||||
for variant in PromptContextSemanticType::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,15 @@ use utoipa::ToSchema;
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PromptType {
|
||||
/// Artcraft (App)
|
||||
ArtcraftApp,
|
||||
|
||||
/// Stable diffusion
|
||||
#[deprecated]
|
||||
StableDiffusion,
|
||||
|
||||
/// Comfy UI
|
||||
#[deprecated]
|
||||
ComfyUi,
|
||||
}
|
||||
|
||||
@@ -29,6 +34,7 @@ impl_mysql_from_row!(PromptType);
|
||||
impl PromptType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ArtcraftApp => "artcraft_app",
|
||||
Self::StableDiffusion => "stable_diffusion",
|
||||
Self::ComfyUi => "comfy_ui",
|
||||
}
|
||||
@@ -36,6 +42,7 @@ impl PromptType {
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"artcraft_app" => Ok(Self::ArtcraftApp),
|
||||
"stable_diffusion" => Ok(Self::StableDiffusion),
|
||||
"comfy_ui" => Ok(Self::ComfyUi),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
@@ -46,6 +53,7 @@ impl PromptType {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::ArtcraftApp,
|
||||
Self::StableDiffusion,
|
||||
Self::ComfyUi,
|
||||
])
|
||||
@@ -62,6 +70,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(PromptType::ArtcraftApp, "artcraft_app");
|
||||
assert_serialization(PromptType::StableDiffusion, "stable_diffusion");
|
||||
assert_serialization(PromptType::ComfyUi, "comfy_ui");
|
||||
}
|
||||
@@ -72,12 +81,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(PromptType::ArtcraftApp.to_str(), "artcraft_app");
|
||||
assert_eq!(PromptType::StableDiffusion.to_str(), "stable_diffusion");
|
||||
assert_eq!(PromptType::ComfyUi.to_str(), "comfy_ui");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(PromptType::from_str("artcraft_app").unwrap(), PromptType::ArtcraftApp);
|
||||
assert_eq!(PromptType::from_str("stable_diffusion").unwrap(), PromptType::StableDiffusion);
|
||||
assert_eq!(PromptType::from_str("comfy_ui").unwrap(), PromptType::ComfyUi);
|
||||
assert!(PromptType::from_str("foo").is_err());
|
||||
@@ -90,7 +101,8 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = PromptType::all_variants();
|
||||
assert_eq!(variants.len(), 2);
|
||||
assert_eq!(variants.len(), 3);
|
||||
assert_eq!(variants.pop_first(), Some(PromptType::ArtcraftApp));
|
||||
assert_eq!(variants.pop_first(), Some(PromptType::StableDiffusion));
|
||||
assert_eq!(variants.pop_first(), Some(PromptType::ComfyUi));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
|
||||
@@ -6,6 +6,8 @@ use strum::EnumCount;
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// NB: This will be used by a variety of tables (MySQL and sqlite)!
|
||||
/// Keep the max length to 16 characters.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -108,5 +110,15 @@ mod tests {
|
||||
assert_eq!(variant, GenerationProvider::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 16;
|
||||
for variant in GenerationProvider::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod generation_provider;
|
||||
pub mod job_status;
|
||||
pub mod job_status_plus;
|
||||
pub mod model_type;
|
||||
pub mod sqlite;
|
||||
pub mod view_as;
|
||||
pub mod visibility;
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// NB: This will be used by a variety of tables (MySQL and sqlite)!
|
||||
/// Keep the max length to 24 characters.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ModelType {
|
||||
// Image models
|
||||
#[serde(rename = "flux_1_dev")]
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[serde(rename = "flux_pro_1p1")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_1p1_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
|
||||
// Video models
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2p1_pro")]
|
||||
Kling21Pro,
|
||||
#[serde(rename = "kling_2p1_master")]
|
||||
Kling21Master,
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
#[serde(rename = "veo_2")]
|
||||
Veo2,
|
||||
|
||||
// 3D Object generation models
|
||||
#[serde(rename = "hunyuan_3d_2p0")]
|
||||
Hunyuan3d2_0,
|
||||
#[serde(rename = "hunyuan_3d_2p1")]
|
||||
Hunyuan3d2_1,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(ModelType);
|
||||
impl_mysql_enum_coders!(ModelType);
|
||||
impl_mysql_from_row!(ModelType);
|
||||
|
||||
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
|
||||
|
||||
impl ModelType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
// Image models
|
||||
Self::Flux1Dev => "flux_1_dev",
|
||||
Self::Flux1Schnell => "flux_1_schnell",
|
||||
Self::FluxPro11 => "flux_pro_1p1",
|
||||
Self::FluxPro11Ultra => "flux_pro_1p1_ultra",
|
||||
Self::GptImage1 => "gpt_image_1",
|
||||
Self::Recraft3 => "recraft_3",
|
||||
|
||||
// Video models
|
||||
Self::Kling16Pro => "kling_1p6_pro",
|
||||
Self::Kling21Pro => "kling_2p1_pro",
|
||||
Self::Kling21Master => "kling_2p1_master",
|
||||
Self::Seedance10Lite => "seedance_1p0_lite",
|
||||
Self::Veo2 => "veo_2",
|
||||
|
||||
// 3D Object generation models
|
||||
Self::Hunyuan3d2_0 => "hunyuan_3d_2p0",
|
||||
Self::Hunyuan3d2_1 => "hunyuan_3d_2p1",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(job_status: &str) -> Result<Self, String> {
|
||||
match job_status {
|
||||
// Image models
|
||||
"flux_1_dev" => Ok(Self::Flux1Dev),
|
||||
"flux_1_schnell" => Ok(Self::Flux1Schnell),
|
||||
"flux_pro_1p1" => Ok(Self::FluxPro11),
|
||||
"flux_pro_1p1_ultra" => Ok(Self::FluxPro11Ultra),
|
||||
"gpt_image_1" => Ok(Self::GptImage1),
|
||||
"recraft_3" => Ok(Self::Recraft3),
|
||||
|
||||
// Video models
|
||||
"kling_1p6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2p1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2p1_master" => Ok(Self::Kling21Master),
|
||||
"seedance_1p0_lite" => Ok(Self::Seedance10Lite),
|
||||
"veo_2" => Ok(Self::Veo2),
|
||||
|
||||
// 3D Object generation models
|
||||
"hunyuan_3d_2p0" => Ok(Self::Hunyuan3d2_0),
|
||||
"hunyuan_3d_2p1" => Ok(Self::Hunyuan3d2_1),
|
||||
|
||||
_ => Err(format!("invalid task_state: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
// Image models
|
||||
Self::Flux1Dev,
|
||||
Self::Flux1Schnell,
|
||||
Self::FluxPro11,
|
||||
Self::FluxPro11Ultra,
|
||||
Self::GptImage1,
|
||||
Self::Recraft3,
|
||||
|
||||
// Video models
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
Self::Seedance10Lite,
|
||||
Self::Veo2,
|
||||
|
||||
// 3D Object generation models
|
||||
Self::Hunyuan3d2_0,
|
||||
Self::Hunyuan3d2_1,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::model_type::ModelType;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
// Image models
|
||||
assert_serialization(ModelType::Flux1Dev, "flux_1_dev");
|
||||
assert_serialization(ModelType::Flux1Schnell, "flux_1_schnell");
|
||||
assert_serialization(ModelType::FluxPro11, "flux_pro_1p1");
|
||||
assert_serialization(ModelType::FluxPro11Ultra, "flux_pro_1p1_ultra");
|
||||
assert_serialization(ModelType::GptImage1, "gpt_image_1");
|
||||
assert_serialization(ModelType::Recraft3, "recraft_3");
|
||||
// Video models
|
||||
assert_serialization(ModelType::Kling16Pro, "kling_1p6_pro");
|
||||
assert_serialization(ModelType::Kling21Pro, "kling_2p1_pro");
|
||||
assert_serialization(ModelType::Kling21Master, "kling_2p1_master");
|
||||
assert_serialization(ModelType::Seedance10Lite, "seedance_1p0_lite");
|
||||
assert_serialization(ModelType::Veo2, "veo_2");
|
||||
// 3D Object generation models
|
||||
assert_serialization(ModelType::Hunyuan3d2_0, "hunyuan_3d_2p0");
|
||||
assert_serialization(ModelType::Hunyuan3d2_1, "hunyuan_3d_2p1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
// Image models
|
||||
assert_eq!(ModelType::Flux1Dev.to_str(), "flux_1_dev");
|
||||
assert_eq!(ModelType::Flux1Schnell.to_str(), "flux_1_schnell");
|
||||
assert_eq!(ModelType::FluxPro11.to_str(), "flux_pro_1p1");
|
||||
assert_eq!(ModelType::FluxPro11Ultra.to_str(), "flux_pro_1p1_ultra");
|
||||
assert_eq!(ModelType::GptImage1.to_str(), "gpt_image_1");
|
||||
assert_eq!(ModelType::Recraft3.to_str(), "recraft_3");
|
||||
|
||||
// Video models
|
||||
assert_eq!(ModelType::Kling16Pro.to_str(), "kling_1p6_pro");
|
||||
assert_eq!(ModelType::Kling21Pro.to_str(), "kling_2p1_pro");
|
||||
assert_eq!(ModelType::Kling21Master.to_str(), "kling_2p1_master");
|
||||
assert_eq!(ModelType::Seedance10Lite.to_str(), "seedance_1p0_lite");
|
||||
assert_eq!(ModelType::Veo2.to_str(), "veo_2");
|
||||
|
||||
// 3D Object generation models
|
||||
assert_eq!(ModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2p0");
|
||||
assert_eq!(ModelType::Hunyuan3d2_1.to_str(), "hunyuan_3d_2p1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
// Image models
|
||||
assert_eq!(ModelType::from_str("flux_1_dev").unwrap(), ModelType::Flux1Dev);
|
||||
assert_eq!(ModelType::from_str("flux_1_schnell").unwrap(), ModelType::Flux1Schnell);
|
||||
assert_eq!(ModelType::from_str("flux_pro_1p1").unwrap(), ModelType::FluxPro11);
|
||||
assert_eq!(ModelType::from_str("flux_pro_1p1_ultra").unwrap(), ModelType::FluxPro11Ultra);
|
||||
assert_eq!(ModelType::from_str("gpt_image_1").unwrap(), ModelType::GptImage1);
|
||||
assert_eq!(ModelType::from_str("recraft_3").unwrap(), ModelType::Recraft3);
|
||||
// Video models
|
||||
assert_eq!(ModelType::from_str("kling_1p6_pro").unwrap(), ModelType::Kling16Pro);
|
||||
assert_eq!(ModelType::from_str("kling_2p1_pro").unwrap(), ModelType::Kling21Pro);
|
||||
assert_eq!(ModelType::from_str("kling_2p1_master").unwrap(), ModelType::Kling21Master);
|
||||
assert_eq!(ModelType::from_str("seedance_1p0_lite").unwrap(), ModelType::Seedance10Lite);
|
||||
assert_eq!(ModelType::from_str("veo_2").unwrap(), ModelType::Veo2);
|
||||
// 3D Object generation models
|
||||
assert_eq!(ModelType::from_str("hunyuan_3d_2p0").unwrap(), ModelType::Hunyuan3d2_0);
|
||||
assert_eq!(ModelType::from_str("hunyuan_3d_2p1").unwrap(), ModelType::Hunyuan3d2_1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = ModelType::all_variants();
|
||||
assert_eq!(variants.len(), 13);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::FluxPro11));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::FluxPro11Ultra));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::GptImage1));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Recraft3));
|
||||
// Video models
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Kling16Pro));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Kling21Pro));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Kling21Master));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Seedance10Lite));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Veo2));
|
||||
// 3D Object generation models
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Hunyuan3d2_0));
|
||||
assert_eq!(variants.pop_first(), Some(ModelType::Hunyuan3d2_1));
|
||||
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(ModelType::all_variants().len(), ModelType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in ModelType::all_variants() {
|
||||
// Test to_str(), from_str(), Display, and Debug.
|
||||
assert_eq!(variant, ModelType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, ModelType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, ModelType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 24;
|
||||
for variant in ModelType::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod task_model_type;
|
||||
pub mod task_status;
|
||||
pub mod task_type;
|
||||
@@ -0,0 +1,241 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskModelType {
|
||||
// Image models
|
||||
#[serde(rename = "flux_1_dev")]
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[serde(rename = "flux_pro_1.1")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_1.1_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
|
||||
// Video models
|
||||
#[serde(rename = "kling_1.6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2.1_pro")]
|
||||
Kling21Pro,
|
||||
#[serde(rename = "kling_2.1_master")]
|
||||
Kling21Master,
|
||||
#[serde(rename = "seedance_1.0_lite")]
|
||||
Seedance10Lite,
|
||||
#[serde(rename = "veo_2")]
|
||||
Veo2,
|
||||
|
||||
// 3D Object generation models
|
||||
#[serde(rename = "hunyuan_3d_2.0")]
|
||||
Hunyuan3d2_0,
|
||||
#[serde(rename = "hunyuan_3d_2.1")]
|
||||
Hunyuan3d2_1,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(TaskModelType);
|
||||
//impl_mysql_enum_coders!(TaskModelType);
|
||||
//impl_mysql_from_row!(TaskModelType);
|
||||
|
||||
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
|
||||
|
||||
impl TaskModelType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
// Image models
|
||||
Self::Flux1Dev => "flux_1_dev",
|
||||
Self::Flux1Schnell => "flux_1_schnell",
|
||||
Self::FluxPro11 => "flux_pro_1.1",
|
||||
Self::FluxPro11Ultra => "flux_pro_1.1_ultra",
|
||||
Self::GptImage1 => "gpt_image_1",
|
||||
Self::Recraft3 => "recraft_3",
|
||||
// Video models
|
||||
Self::Kling16Pro => "kling_1.6_pro",
|
||||
Self::Kling21Pro => "kling_2.1_pro",
|
||||
Self::Kling21Master => "kling_2.1_master",
|
||||
Self::Seedance10Lite => "seedance_1.0_lite",
|
||||
Self::Veo2 => "veo_2",
|
||||
// 3D Object generation models
|
||||
Self::Hunyuan3d2_0 => "hunyuan_3d_2.0",
|
||||
Self::Hunyuan3d2_1 => "hunyuan_3d_2.1",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(job_status: &str) -> Result<Self, String> {
|
||||
match job_status {
|
||||
// Image models
|
||||
"flux_1_dev" => Ok(Self::Flux1Dev),
|
||||
"flux_1_schnell" => Ok(Self::Flux1Schnell),
|
||||
"flux_pro_1.1" => Ok(Self::FluxPro11),
|
||||
"flux_pro_1.1_ultra" => Ok(Self::FluxPro11Ultra),
|
||||
"gpt_image_1" => Ok(Self::GptImage1),
|
||||
"recraft_3" => Ok(Self::Recraft3),
|
||||
// Video models
|
||||
"kling_1.6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2.1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2.1_master" => Ok(Self::Kling21Master),
|
||||
"seedance_1.0_lite" => Ok(Self::Seedance10Lite),
|
||||
"veo_2" => Ok(Self::Veo2),
|
||||
// 3D Object generation models
|
||||
"hunyuan_3d_2.0" => Ok(Self::Hunyuan3d2_0),
|
||||
"hunyuan_3d_2.1" => Ok(Self::Hunyuan3d2_1),
|
||||
_ => Err(format!("invalid task_state: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
// Image models
|
||||
Self::Flux1Dev,
|
||||
Self::Flux1Schnell,
|
||||
Self::FluxPro11,
|
||||
Self::FluxPro11Ultra,
|
||||
Self::GptImage1,
|
||||
Self::Recraft3,
|
||||
// Video models
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
Self::Seedance10Lite,
|
||||
Self::Veo2,
|
||||
// 3D Object generation models
|
||||
Self::Hunyuan3d2_0,
|
||||
Self::Hunyuan3d2_1,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_helpers::assert_serialization;
|
||||
use crate::tauri::tasks::task_model_type::TaskModelType;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
// Image models
|
||||
assert_serialization(TaskModelType::Flux1Dev, "flux_1_dev");
|
||||
assert_serialization(TaskModelType::Flux1Schnell, "flux_1_schnell");
|
||||
assert_serialization(TaskModelType::FluxPro11, "flux_pro_1.1");
|
||||
assert_serialization(TaskModelType::FluxPro11Ultra, "flux_pro_1.1_ultra");
|
||||
assert_serialization(TaskModelType::GptImage1, "gpt_image_1");
|
||||
assert_serialization(TaskModelType::Recraft3, "recraft_3");
|
||||
// Video models
|
||||
assert_serialization(TaskModelType::Kling16Pro, "kling_1.6_pro");
|
||||
assert_serialization(TaskModelType::Kling21Pro, "kling_2.1_pro");
|
||||
assert_serialization(TaskModelType::Kling21Master, "kling_2.1_master");
|
||||
assert_serialization(TaskModelType::Seedance10Lite, "seedance_1.0_lite");
|
||||
assert_serialization(TaskModelType::Veo2, "veo_2");
|
||||
// 3D Object generation models
|
||||
assert_serialization(TaskModelType::Hunyuan3d2_0, "hunyuan_3d_2.0");
|
||||
assert_serialization(TaskModelType::Hunyuan3d2_1, "hunyuan_3d_2.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
// Image models
|
||||
assert_eq!(TaskModelType::Flux1Dev.to_str(), "flux_1_dev");
|
||||
assert_eq!(TaskModelType::Flux1Schnell.to_str(), "flux_1_schnell");
|
||||
assert_eq!(TaskModelType::FluxPro11.to_str(), "flux_pro_1.1");
|
||||
assert_eq!(TaskModelType::FluxPro11Ultra.to_str(), "flux_pro_1.1_ultra");
|
||||
assert_eq!(TaskModelType::GptImage1.to_str(), "gpt_image_1");
|
||||
assert_eq!(TaskModelType::Recraft3.to_str(), "recraft_3");
|
||||
// Video models
|
||||
assert_eq!(TaskModelType::Kling16Pro.to_str(), "kling_1.6_pro");
|
||||
assert_eq!(TaskModelType::Kling21Pro.to_str(), "kling_2.1_pro");
|
||||
assert_eq!(TaskModelType::Kling21Master.to_str(), "kling_2.1_master");
|
||||
assert_eq!(TaskModelType::Seedance10Lite.to_str(), "seedance_1.0_lite");
|
||||
assert_eq!(TaskModelType::Veo2.to_str(), "veo_2");
|
||||
// 3D Object generation models
|
||||
assert_eq!(TaskModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2.0");
|
||||
assert_eq!(TaskModelType::Hunyuan3d2_1.to_str(), "hunyuan_3d_2.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
// Image models
|
||||
assert_eq!(TaskModelType::from_str("flux_1_dev").unwrap(), TaskModelType::Flux1Dev);
|
||||
assert_eq!(TaskModelType::from_str("flux_1_schnell").unwrap(), TaskModelType::Flux1Schnell);
|
||||
assert_eq!(TaskModelType::from_str("flux_pro_1.1").unwrap(), TaskModelType::FluxPro11);
|
||||
assert_eq!(TaskModelType::from_str("flux_pro_1.1_ultra").unwrap(), TaskModelType::FluxPro11Ultra);
|
||||
assert_eq!(TaskModelType::from_str("gpt_image_1").unwrap(), TaskModelType::GptImage1);
|
||||
assert_eq!(TaskModelType::from_str("recraft_3").unwrap(), TaskModelType::Recraft3);
|
||||
// Video models
|
||||
assert_eq!(TaskModelType::from_str("kling_1.6_pro").unwrap(), TaskModelType::Kling16Pro);
|
||||
assert_eq!(TaskModelType::from_str("kling_2.1_pro").unwrap(), TaskModelType::Kling21Pro);
|
||||
assert_eq!(TaskModelType::from_str("kling_2.1_master").unwrap(), TaskModelType::Kling21Master);
|
||||
assert_eq!(TaskModelType::from_str("seedance_1.0_lite").unwrap(), TaskModelType::Seedance10Lite);
|
||||
assert_eq!(TaskModelType::from_str("veo_2").unwrap(), TaskModelType::Veo2);
|
||||
// 3D Object generation models
|
||||
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.0").unwrap(), TaskModelType::Hunyuan3d2_0);
|
||||
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.1").unwrap(), TaskModelType::Hunyuan3d2_1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = TaskModelType::all_variants();
|
||||
assert_eq!(variants.len(), 13);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxPro11));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxPro11Ultra));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::GptImage1));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Recraft3));
|
||||
// Video models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling16Pro));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Pro));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Master));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Seedance10Lite));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Veo2));
|
||||
// 3D Object generation models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_0));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_1));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(TaskModelType::all_variants().len(), TaskModelType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in TaskModelType::all_variants() {
|
||||
// Test to_str(), from_str(), Display, and Debug.
|
||||
assert_eq!(variant, TaskModelType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, TaskModelType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, TaskModelType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 24;
|
||||
for variant in TaskModelType::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ pub fn run_http_server(args: CreateServerArgs) -> AnyhowResult<Server>
|
||||
// NB: We shouldn't be logging much as the /_status endpoint is all we aim to expose.
|
||||
let log_format = "[%{HOSTNAME}e] IP=[%{X-Forwarded-For}i] \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T";
|
||||
|
||||
info!("HTTP server will bind to: {}", bind_address);
|
||||
|
||||
let handle = HttpServer::new(move || {
|
||||
// NB: app_data being clone()'d below should all be safe (dependencies included)
|
||||
App::new()
|
||||
|
||||
@@ -15,6 +15,8 @@ use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginM
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::model_type::ModelType;
|
||||
use filesys::path_to_string::path_to_string;
|
||||
use mysql_queries::payloads::media_file_extra_info::inner_payloads::stable_diffusion_extra_info::StableDiffusionExtraInfo;
|
||||
use mysql_queries::payloads::media_file_extra_info::media_file_extra_info::MediaFileExtraInfo;
|
||||
@@ -328,6 +330,8 @@ pub async fn process_job_inference(
|
||||
maybe_apriori_prompt_token: Some(&prompt_token),
|
||||
prompt_type: PromptType::StableDiffusion,
|
||||
maybe_creator_user_token: job.maybe_creator_user_token_typed.as_ref(),
|
||||
maybe_model_type: None,
|
||||
maybe_generation_provider: None,
|
||||
maybe_positive_prompt: Some(&positive_prompt),
|
||||
maybe_negative_prompt: maybe_negative_prompt.as_deref(),
|
||||
maybe_other_args: None, // TODO(bt,2024-02-22): Support other arguments
|
||||
|
||||
@@ -8,6 +8,8 @@ use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path:
|
||||
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::model_type::ModelType;
|
||||
use filesys::file_size::file_size;
|
||||
use filesys::path_to_string::path_to_string;
|
||||
use hashing::sha256::sha256_hash_file::sha256_hash_file;
|
||||
@@ -290,6 +292,8 @@ pub async fn validate_and_save_results(args: SaveResultsArgs<'_>) -> Result<Medi
|
||||
maybe_apriori_prompt_token: Some(&prompt_token),
|
||||
prompt_type: PromptType::ComfyUi,
|
||||
maybe_creator_user_token: args.job.maybe_creator_user_token_typed.as_ref(),
|
||||
maybe_model_type: None,
|
||||
maybe_generation_provider: None,
|
||||
maybe_positive_prompt: args.comfy_args.positive_prompt.as_deref(),
|
||||
maybe_negative_prompt: args.comfy_args.negative_prompt.as_deref(),
|
||||
maybe_other_args: maybe_other_args.as_ref(),
|
||||
|
||||
@@ -53,6 +53,7 @@ use crate::state::job_dependencies::{BucketDependencies, ClientDependencies, Dat
|
||||
use crate::state::job_specific_dependencies::JobSpecificDependencies;
|
||||
use crate::state::scoped_job_type_execution::ScopedJobTypeExecution;
|
||||
use crate::state::scoped_model_type_execution::ScopedModelTypeExecution;
|
||||
use crate::threads::reap_jobs_thread::reap_jobs_thread;
|
||||
use crate::util::filesystem::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::util::instrumentation::JobInstruments;
|
||||
use crate::util::instrumentation::{init_otel_metrics_pipeline, JobInstrumentLabels};
|
||||
@@ -61,6 +62,7 @@ use crate::util::model_weights_cache::model_weights_cache_directory::ModelWeight
|
||||
pub mod http_server;
|
||||
pub mod job;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod util;
|
||||
|
||||
// Buckets (shared config)
|
||||
@@ -163,6 +165,8 @@ async fn main() -> AnyhowResult<()> {
|
||||
.connect(&db_connection_string)
|
||||
.await?;
|
||||
|
||||
let mysql_pool_2 = mysql_pool.clone();
|
||||
|
||||
info!("Connected to MySQL.");
|
||||
|
||||
let common_env = CommonEnv::read_from_env()?;
|
||||
@@ -345,6 +349,14 @@ async fn main() -> AnyhowResult<()> {
|
||||
|
||||
set_up_directories(&job_dependencies)?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let actix_runtime = actix_web::rt::System::new();
|
||||
|
||||
actix_runtime.block_on(reap_jobs_thread(mysql_pool_2));
|
||||
|
||||
warn!("Reap jobs thread terminated");
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let actix_runtime = actix_web::rt::System::new();
|
||||
let http_server_handle = launch_http_server(create_server_args);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod reap_jobs_thread;
|
||||
@@ -0,0 +1,33 @@
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use mysql_queries::queries::generic_inference::job::reap_stale_fakeyou_jobs::reap_stale_fakeyou_jobs;
|
||||
use sqlx::MySqlPool;
|
||||
use std::time::Duration;
|
||||
|
||||
const WAIT_BETWEEN_REAPING_SECONDS: u64 = 60 * 2; // 2 minutes
|
||||
|
||||
pub async fn reap_jobs_thread(
|
||||
mysql_pool: MySqlPool,
|
||||
) -> ! {
|
||||
info!("Database job reaping thread starting...");
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(WAIT_BETWEEN_REAPING_SECONDS)).await;
|
||||
|
||||
info!("Reaping jobs from database...");
|
||||
|
||||
let result = do_reap_jobs(&mysql_pool).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Error reaping jobs: {:?}", err);
|
||||
tokio::time::sleep(Duration::from_secs(300)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn do_reap_jobs(
|
||||
mysql_pool: &MySqlPool,
|
||||
) -> AnyhowResult<()> {
|
||||
reap_stale_fakeyou_jobs(mysql_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,6 +9,9 @@ use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest, GptImage1EditImageResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::model_type::ModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::enqueue_gpt_image_1_edit_image_webhook;
|
||||
@@ -20,7 +23,9 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
|
||||
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::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens;
|
||||
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
|
||||
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
|
||||
use sqlx::Acquire;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Gpt Image 1 Image Editing
|
||||
@@ -40,9 +45,17 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
request: Json<GptImage1EditImageRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GptImage1EditImageResponse>, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("MySql pool error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
@@ -77,8 +90,8 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
}
|
||||
};
|
||||
|
||||
let result = batch_get_media_files_by_tokens(
|
||||
&server_state.mysql_pool,
|
||||
let result = batch_get_media_files_by_tokens_with_connection(
|
||||
&mut mysql_connection,
|
||||
tokens,
|
||||
CAN_SEE_DELETED,
|
||||
).await;
|
||||
@@ -116,7 +129,7 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
@@ -180,16 +193,51 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error starting MySQL transaction: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
// NB: Don't fail the job if the query fails.
|
||||
let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
maybe_apriori_prompt_token: None,
|
||||
prompt_type: PromptType::ArtcraftApp,
|
||||
maybe_creator_user_token: maybe_user_session
|
||||
.as_ref()
|
||||
.map(|s| &s.user_token),
|
||||
maybe_model_type: Some(ModelType::GptImage1),
|
||||
maybe_generation_provider: Some(GenerationProvider::Artcraft),
|
||||
maybe_positive_prompt: request.prompt.as_deref(),
|
||||
maybe_negative_prompt: None,
|
||||
maybe_other_args: None,
|
||||
creator_ip_address: &ip_address,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let prompt_token = match prompt_result {
|
||||
Ok(token) => Some(token),
|
||||
Err(err) => {
|
||||
warn!("Error inserting prompt: {:?}", err);
|
||||
None // Don't fail the job if the prompt insertion fails.
|
||||
}
|
||||
};
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
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,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
@@ -199,6 +247,14 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let _r = transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error committing MySQL transaction: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(GptImage1EditImageResponse {
|
||||
success: true,
|
||||
|
||||
@@ -120,11 +120,13 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -119,12 +119,14 @@ pub async fn generate_flux_1_schnell_text_to_image_handler(
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_prompt_token: None,
|
||||
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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -121,11 +121,13 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -121,11 +121,13 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -145,11 +145,13 @@ pub async fn remove_image_background_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::BackgroundRemoval,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -146,11 +146,13 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ObjectGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -146,11 +146,13 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ObjectGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -191,11 +191,13 @@ pub async fn generate_kling_1_6_pro_video_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -192,11 +192,13 @@ pub async fn generate_kling_2_1_master_video_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -192,11 +192,13 @@ pub async fn generate_kling_2_1_pro_video_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -171,11 +171,13 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: 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,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
|
||||
@@ -2,16 +2,21 @@ use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::analytics::log_browser_session_handler::LogBrowserSessionError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::model_type::ModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::enqueue_veo_2_image_to_video_webhook;
|
||||
use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2Args;
|
||||
@@ -23,7 +28,10 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
|
||||
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;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_connection;
|
||||
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
|
||||
use sqlx::Acquire;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Veo 2 Image to Video
|
||||
@@ -43,9 +51,17 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
request: Json<GenerateVeo2ImageToVideoRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateVeo2ImageToVideoResponse>, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("MySql pool error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
@@ -78,7 +94,7 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
@@ -86,10 +102,10 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
})?;
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
let media_file_lookup_result = get_media_file(
|
||||
let media_file_lookup_result = get_media_file_with_connection(
|
||||
media_file_token,
|
||||
IS_MOD,
|
||||
&server_state.mysql_pool,
|
||||
&mut mysql_connection,
|
||||
).await;
|
||||
|
||||
let media_file = match media_file_lookup_result {
|
||||
@@ -169,16 +185,53 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error starting MySQL transaction: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
// NB: Don't fail the job if the query fails.
|
||||
let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
maybe_apriori_prompt_token: None,
|
||||
prompt_type: PromptType::ArtcraftApp,
|
||||
maybe_creator_user_token: maybe_user_session
|
||||
.as_ref()
|
||||
.map(|s| &s.user_token),
|
||||
maybe_model_type: Some(ModelType::Veo2),
|
||||
maybe_generation_provider: Some(GenerationProvider::Artcraft),
|
||||
maybe_positive_prompt: Some(prompt),
|
||||
maybe_negative_prompt: None,
|
||||
maybe_other_args: None,
|
||||
creator_ip_address: &ip_address,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let prompt_token = match prompt_result {
|
||||
Ok(token) => Some(token),
|
||||
Err(err) => {
|
||||
warn!("Error inserting prompt: {:?}", err);
|
||||
None // Don't fail the job if the prompt insertion fails.
|
||||
}
|
||||
};
|
||||
|
||||
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_prompt_token: prompt_token.as_ref(),
|
||||
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,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
@@ -189,6 +242,14 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
}
|
||||
};
|
||||
|
||||
let _r = transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error committing MySQL transaction: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(GenerateVeo2ImageToVideoResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
|
||||
@@ -86,6 +86,7 @@ pub async fn handle_image_payload(
|
||||
.media_file_class(MediaFileClass::Image)
|
||||
.media_file_type(media_file_type)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
|
||||
@@ -109,6 +109,7 @@ pub async fn upload_image(
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.frame_width(image_info.width())
|
||||
.frame_height(image_info.height())
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
@@ -89,6 +89,7 @@ pub async fn handle_model_mesh_payload(
|
||||
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
@@ -119,6 +119,7 @@ pub async fn handle_video_payload(
|
||||
.maybe_frame_height(maybe_frame_height)
|
||||
.maybe_duration_millis(maybe_duration_millis)
|
||||
.checksum_sha2(&file_hash)
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 463 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Flux</title><path d="M0 20.683L12.01 2.5 24 20.683h-2.233L12.009 5.878 3.471 18.806h12.122l1.239 1.877H0z"></path><path d="M8.069 16.724l2.073-3.115 2.074 3.115H8.069zM18.24 20.683l-5.668-8.707h2.177l5.686 8.707h-2.196zM19.74 11.676l2.13-3.19 2.13 3.19h-4.26z"></path></svg>
|
||||
|
After Width: | Height: | Size: 438 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kling</title><path clip-rule="evenodd" d="M5.493 21.234c-1.112-1.451-1.109-4.263-.081-7.459l-4.557-2.63a1.683 1.683 0 01-.85-1.304 1.505 1.505 0 01.08-.622 13.18 13.18 0 011.037-2.255c3.476-6.02 10.916-8.23 16.619-4.938.46.266.82.67 1.081 1.184.785 1.545.685 4.096-.234 6.954l4.557 2.631c.339.196.596.492.736.832a1.53 1.53 0 01.034 1.093 13.146 13.146 0 01-1.037 2.255c-3.476 6.02-10.916 8.23-16.619 4.938a2.6 2.6 0 01-.766-.68zm11.096-6.615c-2.073 3.591-5.808 5.316-8.343 3.852-1.267-.731-1.994-2.122-2.145-3.778-.095-1.035.036-2.173.4-3.32.217-.684.517-1.37.902-2.039l.008-.014c2.073-3.59 5.808-5.315 8.343-3.852.633.366 1.13.895 1.49 1.54.986 1.772.922 4.415-.285 6.914-.111.23-.232.457-.362.683l-.008.014z"></path></svg>
|
||||
|
After Width: | Height: | Size: 888 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Midjourney</title><path d="M22.369 17.676c-1.387 1.259-3.17 2.378-5.332 3.417.044.03.086.057.13.083l.018.01.019.012c.216.123.42.184.641.184.222 0 .426-.061.642-.184l.018-.011.019-.011c.14-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.304-.174.612-.266.949-.266.337 0 .645.092.949.266l.023.014c.188.109.334.219.602.442l.178.148c.221.184.346.278.483.36l.028.017.018.01c.21.12.407.181.62.185h.022a.31.31 0 110 .618c-.337 0-.645-.092-.95-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.02-.014a5.356 5.356 0 01-.49-.377l-.159-.132a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.641.184l-.02.011-.018.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.95.266c-.337 0-.644-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.026-.017a4.881 4.881 0 01-.425-.325.308.308 0 01-.12-.1l-.098-.081a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.642.184l-.018.011-.019.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.023.014-.022.014-.09.054A1.868 1.868 0 0112 22c-.337 0-.645-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.021-.014a5.356 5.356 0 01-.49-.377l-.158-.132a3.836 3.836 0 00-.483-.36l-.028-.017-.018-.01a1.256 1.256 0 00-.642-.185c-.221 0-.425.061-.641.184l-.019.011-.018.011c-.141.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.511.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.986.264c-.746-.09-1.319-.38-1.89-.866l-.035-.03c-.047-.041-.118-.106-.192-.174l-.196-.181-.107-.1-.011-.01a1.531 1.531 0 00-.336-.253.313.313 0 00-.095-.03h-.005c-.119.022-.238.059-.361.11a.308.308 0 01-.077.061l-.008.005a.309.309 0 01-.126.034 5.66 5.66 0 00-.774.518l-.416.324-.055.043a6.542 6.542 0 01-.324.236c-.305.207-.552.315-.8.315a.31.31 0 01-.01-.618h.01c.09 0 .235-.062.438-.198l.04-.027c.077-.054.163-.117.27-.199l.385-.301.06-.047c.268-.206.506-.373.73-.505l-.633-1.21a.309.309 0 01.254-.451l20.287-1.305a.309.309 0 01.228.537zm-1.118.14L2.369 19.03l.423.809c.128-.045.256-.078.388-.1a.31.31 0 01.052-.005c.132 0 .26.032.386.093.153.073.294.179.483.35l.016.015.092.086.144.134.097.089c.065.06.125.114.16.144.485.418.948.658 1.554.736h.011a1.25 1.25 0 00.6-.172l.021-.011.019-.011.018-.011c.141-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.305-.174.612-.266.95-.266.336 0 .644.092.948.266l.023.014c.188.109.335.219.603.442l.177.148c.222.184.346.278.484.36l.027.017.019.01c.215.124.42.185.641.185.222 0 .426-.061.641-.184l.019-.011.018-.011c.141-.084.267-.178.493-.366l.177-.148c.28-.232.427-.342.626-.456.304-.174.612-.266.949-.266.337 0 .644.092.949.266l.025.015c.187.109.334.22.603.443 1.867-.878 3.448-1.811 4.73-2.832l.02-.016zM3.653 2.026C6.073 3.06 8.69 4.941 10.8 7.258c2.46 2.7 4.109 5.828 4.637 9.149a.31.31 0 01-.421.335c-2.348-.945-4.54-1.258-6.59-1.02-1.739.2-3.337.792-4.816 1.703-.294.182-.62-.182-.405-.454 1.856-2.355 2.581-4.99 2.343-7.794-.195-2.292-1.031-4.61-2.284-6.709a.31.31 0 01.388-.442zM10.04 4.45c1.778.543 3.892 2.102 5.782 4.243 1.984 2.248 3.552 4.934 4.347 7.582a.31.31 0 01-.401.38l-.022-.01-.386-.154a10.594 10.594 0 00-.291-.112l-.016-.006c-.68-.247-1.199-.291-1.944-.101a.31.31 0 01-.375-.218C15.378 11.123 13.073 7.276 9.775 5c-.291-.201-.072-.653.266-.55zM4.273 2.996l.008.015c1.028 1.94 1.708 4.031 1.885 6.113.213 2.513-.31 4.906-1.673 7.092l-.02.031.003-.001c1.198-.581 2.47-.969 3.825-1.132l.055-.006c1.981-.23 4.083.029 6.309.837l.066.025-.007-.039c-.593-2.95-2.108-5.737-4.31-8.179l-.07-.078c-1.785-1.96-3.944-3.6-6.014-4.65l-.057-.028zm7.92 3.238l.048.048c2.237 2.295 3.885 5.431 4.974 9.191l.038.132.022-.004c.71-.133 1.284-.063 1.963.18l.027.01.066.024.046.018-.025-.073c-.811-2.307-2.208-4.62-3.936-6.594l-.058-.065c-1.02-1.155-2.103-2.132-3.15-2.856l-.015-.011z"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,142 @@
|
||||
import { ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface BadgeInfo {
|
||||
id: string;
|
||||
label?: string | ReactNode;
|
||||
svg?: ReactNode | null;
|
||||
}
|
||||
|
||||
interface ModelBadgeGridProps {
|
||||
className?: string;
|
||||
highlight?: string;
|
||||
rowOffsets?: number[];
|
||||
}
|
||||
|
||||
const baseBadgeClasses =
|
||||
"rounded-2xl px-6 py-3 text-2xl font-normal flex-shrink-0 bg-white/10 text-white/90 text-center h-[58px] flex items-center justify-center gap-1.5";
|
||||
const highlightClasses =
|
||||
"bg-primary/30 text-white font-semibold shadow-lg border-2 border-primary/60";
|
||||
|
||||
const rows: BadgeInfo[][] = [
|
||||
[
|
||||
{ id: "empty1", svg: null },
|
||||
{ id: "empty2", svg: null },
|
||||
{ id: "empty3", svg: null },
|
||||
{
|
||||
id: "flux",
|
||||
label: "Flux Pro 1.1 Ultra",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/flux.svg"
|
||||
alt="Flux logo"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ id: "empty4", svg: null },
|
||||
|
||||
{ id: "empty6", svg: null },
|
||||
{ id: "empty7", svg: null },
|
||||
{ id: "empty8", svg: null },
|
||||
],
|
||||
[
|
||||
{ id: "empty9", svg: null },
|
||||
{
|
||||
id: "midjourney",
|
||||
label: "Midjourney",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/midjourney.svg"
|
||||
alt="Midjourney logo"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "gpt-image-1",
|
||||
label: "GPT-Image 1",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/openai.svg"
|
||||
alt="OpenAI logo"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ id: "empty10", svg: null },
|
||||
|
||||
{ id: "empty11", svg: null },
|
||||
{ id: "empty12", svg: null },
|
||||
{ id: "empty13", svg: null },
|
||||
{ id: "empty14", svg: null },
|
||||
],
|
||||
[
|
||||
{ id: "empty15", svg: null },
|
||||
{ id: "empty16", svg: null },
|
||||
{ id: "empty17", svg: null },
|
||||
{
|
||||
id: "kling",
|
||||
label: "Kling 2.1 Pro",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/kling.svg"
|
||||
alt="Kling AI logo"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ id: "empty18", svg: null },
|
||||
{ id: "empty19", svg: null },
|
||||
{ id: "empty20", svg: null },
|
||||
{ id: "empty21", svg: null },
|
||||
],
|
||||
];
|
||||
|
||||
export default function ModelBadgeGrid({
|
||||
className = "",
|
||||
highlight,
|
||||
rowOffsets = [],
|
||||
}: ModelBadgeGridProps) {
|
||||
return (
|
||||
<div
|
||||
className={`select-none relative z-10 h-full overflow-hidden ${className}`}
|
||||
>
|
||||
{/* Gradient fade overlays */}
|
||||
<div className="absolute left-0 top-0 w-32 xl:w-96 h-full bg-gradient-to-r from-[#28282C] to-transparent z-10 pointer-events-none" />
|
||||
<div className="absolute right-0 top-0 w-32 xl:w-96 h-full bg-gradient-to-l from-[#28282C] to-transparent z-10 pointer-events-none" />
|
||||
|
||||
<div className="flex flex-col gap-5 h-full -mx-[280px] xl:-mx-8">
|
||||
{rows.map((row, rowIdx) => {
|
||||
const offset = rowOffsets[rowIdx] ?? 0;
|
||||
return (
|
||||
<div
|
||||
key={rowIdx}
|
||||
className="flex gap-5 whitespace-nowrap"
|
||||
style={{ marginLeft: offset }}
|
||||
>
|
||||
{row.map(({ id, label, svg }, idx) => {
|
||||
const isHighlight = highlight && highlight.toLowerCase() === id;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={twMerge(
|
||||
baseBadgeClasses,
|
||||
label ? "" : "min-w-[180px]",
|
||||
isHighlight ? highlightClasses : ""
|
||||
)}
|
||||
>
|
||||
{svg ? (
|
||||
<span className="mr-2 w-6 h-6 inline-flex ">{svg}</span>
|
||||
) : null}
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,25 @@ import { motion } from "framer-motion";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { faWindows, faApple } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faVolumeMute, faVolumeHigh } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faVolumeMute,
|
||||
faVolumeHigh,
|
||||
faFilm,
|
||||
faPaintBrush,
|
||||
faCamera,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Footer from "../../components/footer";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import ModelBadgeGrid from "../../components/model-badge-grid";
|
||||
|
||||
//const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.06.04.dmg";
|
||||
const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.07.13.dmg";
|
||||
//const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.07.13.dmg";
|
||||
const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.07.17.dmg";
|
||||
|
||||
//const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.06.04.exe";
|
||||
const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.07.13.exe";
|
||||
//const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.07.13.exe";
|
||||
const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.07.17.exe";
|
||||
|
||||
const fadeUpVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
@@ -48,7 +57,7 @@ const Landing = () => {
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.muted = isMuted;
|
||||
videoRef.current.volume = 0.14;
|
||||
videoRef.current.volume = 0.1;
|
||||
}
|
||||
}, [isMuted]);
|
||||
|
||||
@@ -82,18 +91,21 @@ const Landing = () => {
|
||||
<span className="text-white">
|
||||
Controllable AI
|
||||
<br />
|
||||
for Artists
|
||||
for{" "}
|
||||
<span className="relative after:absolute after:bottom-0 after:left-0 after:w-full after:h-[6px] md:after:h-[11px] after:bg-primary/80 after:mb-0 after:xl:mb-2">
|
||||
Artists
|
||||
</span>
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<motion.p
|
||||
className="text-base sm:text-xl md:text-2xl lg:text-2xl text-white/70 mb-8 sm:mb-12 max-w-2xl mx-auto font-medium drop-shadow-[0_2px_12px_rgba(80,80,255,0.10)]"
|
||||
className="text-base mt-4 sm:text-xl md:text-2xl lg:text-2xl text-white/70 mb-8 sm:mb-12 max-w-2xl mx-auto font-medium drop-shadow-[0_2px_12px_rgba(80,80,255,0.10)]"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
Text prompting sucks. Bring your true vision to life with
|
||||
Text prompting sucks. Bring your true vision to life with
|
||||
unparalleled control and precision.
|
||||
</motion.p>
|
||||
|
||||
@@ -182,6 +194,229 @@ const Landing = () => {
|
||||
</main>
|
||||
|
||||
<div className="relative flex overflow-visible xl:items-center xl:pt-0 mb-4 md:mb-12 px-2 sm:px-4 md:px-0">
|
||||
{/* Gradient Orb for Section */}
|
||||
<div className="absolute left-[-200px] top-[-100px] w-[600px] h-[600px] rounded-full bg-gradient-to-br from-[#00AABA] via-blue-500 to-blue-700 opacity-15 blur-[120px] z-0 pointer-events-none" />
|
||||
<motion.div
|
||||
className="w-full flex flex-col items-center justify-center text-center pt-16 sm:pt-24 md:pt-32 px-2 sm:px-6 md:px-10"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<motion.div className="relative" variants={fadeUpVariants}>
|
||||
<h1 className="relative mb-4 sm:mb-6 md:mb-10 font-bold text-3xl sm:text-4xl md:text-[3rem] lg:text-[4rem] xl:text-[4.5rem] !leading-tight text-shadow-lg">
|
||||
<span
|
||||
className="text-primary"
|
||||
style={{ textShadow: "0 0 15px var(--color-primary)" }}
|
||||
>
|
||||
Five Reasons{" "}
|
||||
</span>
|
||||
Why
|
||||
<br />
|
||||
ArtCraft is the
|
||||
<span
|
||||
className="text-primary"
|
||||
style={{ textShadow: "0 0 15px var(--color-primary)" }}
|
||||
>
|
||||
{" "}
|
||||
Best Tool
|
||||
</span>
|
||||
</h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bento Box Grid Section */}
|
||||
<div className="overflow-visible pb-10 sm:pb-20 md:pb-28 lg:pb-32 relative px-4">
|
||||
{/* Gradient Orbs for Section */}
|
||||
<div className="absolute right-[-150px] top-[-100px] w-[500px] h-[500px] rounded-full bg-gradient-to-br from-[#00AABA] via-blue-500 to-blue-700 opacity-10 blur-[110px] z-0 pointer-events-none" />
|
||||
{/* <div className="absolute right-[-200px] bottom-[-100px] w-[600px] h-[600px] rounded-full bg-gradient-to-br from-blue-700 via-[#00AABA] to-pink-500 opacity-8 blur-[120px] z-0 pointer-events-none" /> */}
|
||||
|
||||
<motion.div
|
||||
className="mx-auto max-w-[88rem] md:px-6 lg:px-8"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
{/* Bento Grid Container */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-12 gap-6 sm:gap-8">
|
||||
{/* Reason #1 - Large Box */}
|
||||
<motion.div
|
||||
className="xl:col-span-6 bg-[#28282C] rounded-2xl sm:rounded-3xl p-6 lg:p-8 shadow-2xl transition-all duration-300 group"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="flex xl:flex-col gap-4 lg:gap-8 h-full flex-col-reverse">
|
||||
<div className="grow h-40">
|
||||
<img
|
||||
src="/images/2d-3d.png"
|
||||
alt="2D and 3D"
|
||||
className="w-full h-full object-cover rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="font-bold text-xl sm:text-2xl lg:text-3xl mb-3 sm:mb-4 leading-tight">
|
||||
Text Prompting Sucks
|
||||
</h3>
|
||||
<p className="text-white/80 text-sm sm:text-base lg:text-lg leading-relaxed">
|
||||
<span className="text-primary-400 font-semibold">
|
||||
Our tool lets you design visually, like Figma.
|
||||
</span>{" "}
|
||||
Draw on a canvas or work in a 3D space as if you're
|
||||
playing a video game.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Reason #2 - Medium Box */}
|
||||
<motion.div
|
||||
className="xl:col-span-6 bg-[#28282C] rounded-2xl sm:rounded-3xl p-6 lg:p-8 pb-0 lg:pb-0 shadow-2xl transition-all duration-300 group"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="relative flex flex-col h-full">
|
||||
<h3 className="font-bold text-xl sm:text-2xl lg:text-3xl mb-3 sm:mb-4 leading-tight ">
|
||||
Desktop App
|
||||
</h3>
|
||||
<p className="text-white/80 text-sm sm:text-base mb-4 sm:mb-6 lg:text-lg leading-relaxed">
|
||||
<span className="text-primary-400 font-semibold">
|
||||
No more hunting for the hundredth tab.
|
||||
</span>{" "}
|
||||
Works on Windows, Mac, and soon Linux and Tablets. First class
|
||||
experience for real artists.
|
||||
</p>
|
||||
<div className="h-20 md:h-24 lg:h-36 xl:h-36 bg-white/[3%] border-[5px] border-white/[2%] rounded-t-2xl relative mt-12 lg:mt-16 xl:mt-24 select-none">
|
||||
<div className="absolute -top-20 left-1/2 -translate-x-1/2 flex gap-9 items-center justify-center drop-shadow-2xl z-20 scale-50 lg:scale-75 xl:scale-100">
|
||||
<img
|
||||
src="/images/windows-logo.png"
|
||||
alt="Windows Logo"
|
||||
draggable={false}
|
||||
className="h-32 rotate-6"
|
||||
/>
|
||||
<img
|
||||
src="/images/apple-logo.png"
|
||||
alt="Windows Logo"
|
||||
draggable={false}
|
||||
className="h-36 -rotate-6"
|
||||
/>
|
||||
<img
|
||||
src="/images/linux-logo.png"
|
||||
alt="Windows Logo"
|
||||
draggable={false}
|
||||
className="h-36 rotate-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-0 bottom-0 w-full h-28 bg-gradient-to-t from-[#28282C] to-transparent z-10 pointer-events-none" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Reason #3 - Medium Box */}
|
||||
<motion.div
|
||||
className="xl:col-span-4 bg-[#28282C] rounded-2xl sm:rounded-3xl p-6 lg:p-8 shadow-2xl transition-all duration-300 group"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<h3 className="font-bold text-xl sm:text-2xl lg:text-3xl mb-3 sm:mb-4 leading-tight">
|
||||
It's Open Source
|
||||
</h3>
|
||||
<p className="text-white/80 text-sm sm:text-base lg:text-lg mb-4 sm:mb-6 leading-relaxed flex-1">
|
||||
We'll be releasing the code behind our desktop app, server,
|
||||
models, and infrastructure as{" "}
|
||||
<span className="text-primary-400 font-semibold">
|
||||
open source!
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex justify-center items-center h-full p-4 lg:p-6 select-none">
|
||||
<img
|
||||
src="/images/github-logo.png"
|
||||
alt="GitHub Logo"
|
||||
className="h-20 md:h-32 lg:h-36"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Reason #4 - Large Box */}
|
||||
<motion.div
|
||||
className="xl:col-span-8 bg-[#28282C] rounded-2xl sm:rounded-3xl shadow-2xl transition-all duration-300 group overflow-hidden"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="lg:flex-1 flex flex-col justify-between">
|
||||
<div className="p-6 lg:p-8">
|
||||
<h3 className="font-bold text-xl sm:text-2xl lg:text-3xl mb-3 sm:mb-4 leading-tight">
|
||||
Use Every Model
|
||||
</h3>
|
||||
<p className="text-white/80 text-sm sm:text-base lg:text-lg leading-relaxed">
|
||||
You'll be able to use{" "}
|
||||
<span className="text-primary-400 font-semibold">
|
||||
EVERY image and video model
|
||||
</span>{" "}
|
||||
all in one place. Log in with your existing subscriptions.
|
||||
</p>
|
||||
</div>
|
||||
<ModelBadgeGrid
|
||||
highlight="gpt-image-1"
|
||||
rowOffsets={[-70, -90, -160]}
|
||||
className="mt-3"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Reason #5 - Full Width Box */}
|
||||
<motion.div
|
||||
className="xl:col-span-12 md:col-span-2 bg-[#28282C] rounded-2xl sm:rounded-3xl p-6 lg:p-8 shadow-2xl transition-all duration-300 group"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row gap-4 lg:gap-8 items-center">
|
||||
<div className="lg:flex-1">
|
||||
<h3 className="font-bold text-xl sm:text-2xl lg:text-3xl mb-3 sm:mb-4 leading-tight">
|
||||
Created by Artists and Filmmakers
|
||||
</h3>
|
||||
<p className="text-white/80 text-sm sm:text-base lg:text-lg leading-relaxed">
|
||||
<span className="text-primary-400 font-semibold">
|
||||
The other leading platforms were created by the Google ad
|
||||
team, crypto bros, and other non-artists.
|
||||
</span>{" "}
|
||||
<br />
|
||||
Not us. We're one of you.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center items-center h-24 lg:h-28">
|
||||
{/* Film icon */}
|
||||
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-pink-900 rounded-full flex items-center justify-center border-2 border-pink-600 shadow-lg z-10">
|
||||
<FontAwesomeIcon
|
||||
icon={faFilm}
|
||||
className="text-white text-xl lg:text-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Paint brush icon - center, most prominent */}
|
||||
<div className="w-20 h-20 lg:w-24 lg:h-24 bg-emerald-600 rounded-full flex items-center justify-center border-2 border-emerald-400 shadow-lg -ml-2 z-30">
|
||||
<FontAwesomeIcon
|
||||
icon={faPaintBrush}
|
||||
className="text-white text-2xl lg:text-3xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Camera icon */}
|
||||
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-purple-900 rounded-full flex items-center justify-center border-2 border-purple-600 shadow-lg -ml-2 z-20">
|
||||
<FontAwesomeIcon
|
||||
icon={faCamera}
|
||||
className="text-white text-xl lg:text-2xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex overflow-visible xl:items-center xl:pt-0 px-2 sm:px-4 md:px-0">
|
||||
{/* Gradient Orb for Section */}
|
||||
<div className="absolute left-[-200px] top-[-100px] w-[600px] h-[600px] rounded-full bg-gradient-to-br from-[#00AABA] via-blue-500 to-blue-700 opacity-15 blur-[120px] z-0 pointer-events-none" />
|
||||
<motion.div
|
||||
@@ -214,7 +449,7 @@ const Landing = () => {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mx-auto w-full max-w-[1200px] px-2 sm:px-4 md:px-16 lg:px-12 xl:px-32 pb-8 sm:pb-16 md:pb-24 lg:pb-32 overflow-visible px-4">
|
||||
<div className="relative z-10 mx-auto w-full max-w-[1200px] sm:px-4 md:px-16 lg:px-12 xl:px-32 pb-8 sm:pb-16 md:pb-24 lg:pb-32 overflow-visible px-4">
|
||||
{/* Gradient Orb for Section */}
|
||||
<div className="absolute right-[-250px] top-[-150px] w-[700px] h-[700px] rounded-full bg-gradient-to-br from-blue-700 via-[#00AABA] to-pink-500 opacity-[0.1] blur-[140px] z-0 pointer-events-none" />
|
||||
<motion.div
|
||||
@@ -247,7 +482,35 @@ const Landing = () => {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-visible py-10 sm:py-20 md:py-28 lg:py-32 relative px-4">
|
||||
<div className="relative flex overflow-visible xl:items-center xl:pt-0 mb-4 md:mb-12 px-2 sm:px-4 md:px-0">
|
||||
{/* Gradient Orb for Section */}
|
||||
<div className="absolute left-[-200px] top-[-100px] w-[600px] h-[600px] rounded-full bg-gradient-to-br from-[#00AABA] via-blue-500 to-blue-700 opacity-15 blur-[120px] z-0 pointer-events-none" />
|
||||
<motion.div
|
||||
className="w-full flex flex-col items-center justify-center text-center pt-16 sm:pt-24 md:pt-32 px-2 sm:px-6 md:px-10"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<motion.div className="relative" variants={fadeUpVariants}>
|
||||
<h1 className="relative mb-4 sm:mb-6 md:mb-10 font-bold text-3xl sm:text-4xl md:text-[3rem] lg:text-[4rem] xl:text-[4.5rem] !leading-none text-shadow-lg">
|
||||
<span
|
||||
className="text-white leading-tight"
|
||||
style={{ textShadow: "0 0 15px var(--color-primary)" }}
|
||||
>
|
||||
We're Pulling You{" "}
|
||||
<span className="text-primary">
|
||||
Out
|
||||
<br />
|
||||
of Prompting
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-visible pb-10 sm:pb-20 md:pb-28 lg:pb-32 relative px-4">
|
||||
{/* Gradient Orb for Section */}
|
||||
<div className="absolute left-[-150px] bottom-[-100px] w-[500px] h-[500px] rounded-full bg-gradient-to-br from-[#00AABA] via-blue-500 to-blue-700 opacity-10 blur-[110px] z-0 pointer-events-none" />
|
||||
<motion.div
|
||||
@@ -263,7 +526,7 @@ const Landing = () => {
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<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">
|
||||
<h2 className="relative font-bold text-4xl md:text-5xl !leading-tight">
|
||||
Our tool lets you design, control, and achieve{" "}
|
||||
<span className="text-primary">perfect consistency</span>
|
||||
</h2>
|
||||
@@ -272,7 +535,7 @@ const Landing = () => {
|
||||
</motion.div>
|
||||
<div className="sm:px-2 sm:mx-auto lg:px-0">
|
||||
<div className="relative isolate overflow-hidden bg-indigo-500 sm:mx-auto sm:max-w-2xl sm:rounded-3xl sm:pr-0 lg:mx-0 lg:max-w-none">
|
||||
<div className="mx-auto max-w-full sm:max-w-2xl sm:mx-0 sm:max-w-none p-2 sm:p-4 bg-white/10 backdrop-blur-md rounded-2xl sm:rounded-3xl overflow-hidden shadow-2xl">
|
||||
<div className="mx-auto max-w-full sm:mx-0 sm:max-w-none p-2 sm:p-4 bg-white/10 backdrop-blur-md rounded-2xl sm:rounded-3xl overflow-hidden shadow-2xl">
|
||||
<video
|
||||
src="/videos/artcraft-3d-demo.mp4"
|
||||
autoPlay
|
||||
@@ -304,7 +567,7 @@ const Landing = () => {
|
||||
className="relative isolate overflow-hidden bg-indigo-500 sm:mx-auto sm:max-w-2xl sm:rounded-3xl sm:pr-0 lg:mx-0 lg:max-w-none"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="mx-auto max-w-full sm:max-w-2xl sm:mx-0 sm:max-w-none p-2 sm:p-4 bg-white/10 backdrop-blur-md rounded-2xl sm:rounded-3xl overflow-hidden shadow-2xl">
|
||||
<div className="mx-auto max-w-full sm:mx-0 sm:max-w-none p-2 sm:p-4 bg-white/10 backdrop-blur-md rounded-2xl sm:rounded-3xl overflow-hidden shadow-2xl">
|
||||
<video
|
||||
src="/videos/artcraft-canvas-demo.mp4"
|
||||
autoPlay
|
||||
@@ -321,9 +584,9 @@ const Landing = () => {
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<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">
|
||||
<h2 className="relative font-bold text-4xl md:text-5xl !leading-tight">
|
||||
<span className="text-primary">Patented</span> Brain to Art
|
||||
Interface lets you Sketch and Composite
|
||||
Interface lets you Sketch and Composite
|
||||
</h2>
|
||||
</div>
|
||||
<DiscordButton />
|
||||
@@ -332,7 +595,7 @@ const Landing = () => {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mx-auto w-full max-w-screen px-2 sm:px-4 py-10 sm:py-20 md:py-32 sm:px-8 lg:px-32 overflow-visible px-4">
|
||||
<div className="relative z-10 mx-auto w-full max-w-screen py-10 sm:py-20 md:py-32 sm:px-8 lg:px-32 overflow-visible px-4">
|
||||
{/* Gradient Orb for Section */}
|
||||
<div className="absolute left-[-250px] top-[-150px] w-[700px] h-[700px] rounded-full bg-gradient-to-br from-[#00AABA] via-blue-500 to-blue-700 opacity-[0.07] blur-[150px] z-0 pointer-events-none" />
|
||||
{/* Videos Section */}
|
||||
@@ -377,7 +640,7 @@ const Landing = () => {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mx-auto w-full max-w-[1400px] px-2 sm:px-4 pt-10 sm:pt-20 md:pt-32 pb-12 sm:pb-24 md:pb-40 lg:pb-56 sm:px-8 lg:px-32 overflow-visible px-4">
|
||||
<div className="relative z-10 mx-auto w-full max-w-[1400px] pt-10 sm:pt-20 md:pt-32 pb-12 sm:pb-24 md:pb-40 lg:pb-56 sm:px-8 lg:px-32 overflow-visible px-4">
|
||||
{/* Gradient Orb for Section */}
|
||||
<div className="absolute right-[-300px] bottom-[-100px] w-[800px] h-[800px] rounded-full bg-gradient-to-br from-blue-700 via-[#00AABA] to-pink-500 opacity-10 blur-[160px] z-0 pointer-events-none" />
|
||||
{/* Discord CTA Section */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { createGlobPatternsForDependencies } = require("@nx/react/tailwind");
|
||||
const { join } = require("path");
|
||||
import colors from "tailwindcss/colors";
|
||||
import colors, { teal } from "tailwindcss/colors";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
@@ -24,6 +24,12 @@ module.exports = {
|
||||
gray: colors.gray,
|
||||
black: colors.black,
|
||||
blue: colors.blue,
|
||||
orange: colors.orange,
|
||||
green: colors.green,
|
||||
emerald: colors.emerald,
|
||||
teal: colors.teal,
|
||||
yellow: colors.yellow,
|
||||
pink: colors.pink,
|
||||
purple: colors.purple,
|
||||
primary: {
|
||||
DEFAULT: "#2d81ff",
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
gtag('config', 'G-Y4Y8KMLPQ7');
|
||||
</script>
|
||||
|
||||
<!-- Microsoft Clarity -->
|
||||
<script type="text/javascript">
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "sixp9uu263");
|
||||
</script>
|
||||
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Flux</title><path d="M0 20.683L12.01 2.5 24 20.683h-2.233L12.009 5.878 3.471 18.806h12.122l1.239 1.877H0z"></path><path d="M8.069 16.724l2.073-3.115 2.074 3.115H8.069zM18.24 20.683l-5.668-8.707h2.177l5.686 8.707h-2.196zM19.74 11.676l2.13-3.19 2.13 3.19h-4.26z"></path></svg>
|
||||
|
After Width: | Height: | Size: 438 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ByteDance</title><path d="M14.944 18.587l-1.704-.445V10.01l1.824-.462c1-.254 1.84-.461 1.88-.453.032 0 .056 2.235.056 4.972v4.973l-.176-.008c-.104 0-.952-.207-1.88-.446z"></path><path d="M7 16.542c0-2.736.024-4.98.064-4.98.032-.008.872.2 1.88.454l1.816.461-.016 4.05-.024 4.049-1.632.422c-.896.23-1.736.445-1.856.469L7 21.523v-4.98z"></path><path d="M19.24 12.477c0-9.03.008-9.515.144-9.475.072.024.784.207 1.576.406.792.207 1.576.405 1.744.445l.296.08-.016 8.56-.024 8.568-1.624.414c-.888.23-1.728.437-1.856.47l-.24.055v-9.523z"></path><path d="M1 12.509c0-4.678.024-8.505.064-8.505.032 0 .872.207 1.872.454l1.824.461v7.582c0 4.16-.016 7.574-.032 7.574-.024 0-.872.215-1.88.47L1 21.013v-8.505z"></path></svg>
|
||||
|
After Width: | Height: | Size: 873 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"></path></svg>
|
||||
|
After Width: | Height: | Size: 651 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kling</title><path clip-rule="evenodd" d="M5.493 21.234c-1.112-1.451-1.109-4.263-.081-7.459l-4.557-2.63a1.683 1.683 0 01-.85-1.304 1.505 1.505 0 01.08-.622 13.18 13.18 0 011.037-2.255c3.476-6.02 10.916-8.23 16.619-4.938.46.266.82.67 1.081 1.184.785 1.545.685 4.096-.234 6.954l4.557 2.631c.339.196.596.492.736.832a1.53 1.53 0 01.034 1.093 13.146 13.146 0 01-1.037 2.255c-3.476 6.02-10.916 8.23-16.619 4.938a2.6 2.6 0 01-.766-.68zm11.096-6.615c-2.073 3.591-5.808 5.316-8.343 3.852-1.267-.731-1.994-2.122-2.145-3.778-.095-1.035.036-2.173.4-3.32.217-.684.517-1.37.902-2.039l.008-.014c2.073-3.59 5.808-5.315 8.343-3.852.633.366 1.13.895 1.49 1.54.986 1.772.922 4.415-.285 6.914-.111.23-.232.457-.362.683l-.008.014z"></path></svg>
|
||||
|
After Width: | Height: | Size: 888 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Midjourney</title><path d="M22.369 17.676c-1.387 1.259-3.17 2.378-5.332 3.417.044.03.086.057.13.083l.018.01.019.012c.216.123.42.184.641.184.222 0 .426-.061.642-.184l.018-.011.019-.011c.14-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.304-.174.612-.266.949-.266.337 0 .645.092.949.266l.023.014c.188.109.334.219.602.442l.178.148c.221.184.346.278.483.36l.028.017.018.01c.21.12.407.181.62.185h.022a.31.31 0 110 .618c-.337 0-.645-.092-.95-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.02-.014a5.356 5.356 0 01-.49-.377l-.159-.132a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.641.184l-.02.011-.018.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.95.266c-.337 0-.644-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.026-.017a4.881 4.881 0 01-.425-.325.308.308 0 01-.12-.1l-.098-.081a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.642.184l-.018.011-.019.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.023.014-.022.014-.09.054A1.868 1.868 0 0112 22c-.337 0-.645-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.021-.014a5.356 5.356 0 01-.49-.377l-.158-.132a3.836 3.836 0 00-.483-.36l-.028-.017-.018-.01a1.256 1.256 0 00-.642-.185c-.221 0-.425.061-.641.184l-.019.011-.018.011c-.141.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.511.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.986.264c-.746-.09-1.319-.38-1.89-.866l-.035-.03c-.047-.041-.118-.106-.192-.174l-.196-.181-.107-.1-.011-.01a1.531 1.531 0 00-.336-.253.313.313 0 00-.095-.03h-.005c-.119.022-.238.059-.361.11a.308.308 0 01-.077.061l-.008.005a.309.309 0 01-.126.034 5.66 5.66 0 00-.774.518l-.416.324-.055.043a6.542 6.542 0 01-.324.236c-.305.207-.552.315-.8.315a.31.31 0 01-.01-.618h.01c.09 0 .235-.062.438-.198l.04-.027c.077-.054.163-.117.27-.199l.385-.301.06-.047c.268-.206.506-.373.73-.505l-.633-1.21a.309.309 0 01.254-.451l20.287-1.305a.309.309 0 01.228.537zm-1.118.14L2.369 19.03l.423.809c.128-.045.256-.078.388-.1a.31.31 0 01.052-.005c.132 0 .26.032.386.093.153.073.294.179.483.35l.016.015.092.086.144.134.097.089c.065.06.125.114.16.144.485.418.948.658 1.554.736h.011a1.25 1.25 0 00.6-.172l.021-.011.019-.011.018-.011c.141-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.305-.174.612-.266.95-.266.336 0 .644.092.948.266l.023.014c.188.109.335.219.603.442l.177.148c.222.184.346.278.484.36l.027.017.019.01c.215.124.42.185.641.185.222 0 .426-.061.641-.184l.019-.011.018-.011c.141-.084.267-.178.493-.366l.177-.148c.28-.232.427-.342.626-.456.304-.174.612-.266.949-.266.337 0 .644.092.949.266l.025.015c.187.109.334.22.603.443 1.867-.878 3.448-1.811 4.73-2.832l.02-.016zM3.653 2.026C6.073 3.06 8.69 4.941 10.8 7.258c2.46 2.7 4.109 5.828 4.637 9.149a.31.31 0 01-.421.335c-2.348-.945-4.54-1.258-6.59-1.02-1.739.2-3.337.792-4.816 1.703-.294.182-.62-.182-.405-.454 1.856-2.355 2.581-4.99 2.343-7.794-.195-2.292-1.031-4.61-2.284-6.709a.31.31 0 01.388-.442zM10.04 4.45c1.778.543 3.892 2.102 5.782 4.243 1.984 2.248 3.552 4.934 4.347 7.582a.31.31 0 01-.401.38l-.022-.01-.386-.154a10.594 10.594 0 00-.291-.112l-.016-.006c-.68-.247-1.199-.291-1.944-.101a.31.31 0 01-.375-.218C15.378 11.123 13.073 7.276 9.775 5c-.291-.201-.072-.653.266-.55zM4.273 2.996l.008.015c1.028 1.94 1.708 4.031 1.885 6.113.213 2.513-.31 4.906-1.673 7.092l-.02.031.003-.001c1.198-.581 2.47-.969 3.825-1.132l.055-.006c1.981-.23 4.083.029 6.309.837l.066.025-.007-.039c-.593-2.95-2.108-5.737-4.31-8.179l-.07-.078c-1.785-1.96-3.944-3.6-6.014-4.65l-.057-.028zm7.92 3.238l.048.048c2.237 2.295 3.885 5.431 4.974 9.191l.038.132.022-.004c.71-.133 1.284-.063 1.963.18l.027.01.066.024.046.018-.025-.073c-.811-2.307-2.208-4.62-3.936-6.594l-.058-.065c-1.02-1.155-2.103-2.132-3.15-2.856l-.015-.011z"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,8 +1,6 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { ListDropdown } from "@storyteller/ui-list-dropdown";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
import { Label } from "@storyteller/ui-label";
|
||||
import { FileUploader } from "@storyteller/ui-file-uploader";
|
||||
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities";
|
||||
import { upload3DObjects } from "./utilities/upload3DObjects";
|
||||
@@ -48,13 +46,6 @@ export const UploadFiles3D = ({
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const [uploadTitle, setUploadTitle] = useState<{
|
||||
value: string;
|
||||
error?: string;
|
||||
}>({
|
||||
value: "",
|
||||
});
|
||||
|
||||
const [assetFile, setAssetFile] = useState<{
|
||||
value: File | null;
|
||||
error?: string;
|
||||
@@ -70,14 +61,6 @@ export const UploadFiles3D = ({
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!uploadTitle.value) {
|
||||
setUploadTitle((curr) => ({
|
||||
...curr,
|
||||
error: "Please enter a title.",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assetFile.value) {
|
||||
setAssetFile((curr) => ({
|
||||
...curr,
|
||||
@@ -86,8 +69,11 @@ export const UploadFiles3D = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the filename as the title
|
||||
const title = assetFile.value.name.split(".")[0];
|
||||
|
||||
upload3DObjects({
|
||||
title: uploadTitle.value,
|
||||
title: title,
|
||||
assetFile: assetFile.value,
|
||||
thumbnailSnapshot: thumbnailFile,
|
||||
engineCategory: engineCategory,
|
||||
@@ -123,18 +109,7 @@ export const UploadFiles3D = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col">
|
||||
<Label required>Asset Name</Label>
|
||||
<Input
|
||||
placeholder="Enter the asset name here"
|
||||
errorMessage={uploadTitle.error}
|
||||
value={uploadTitle.value}
|
||||
onChange={(event) => setUploadTitle({ value: event.target.value })}
|
||||
className={uploadTitle.error ? "mb-3" : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<FileUploader
|
||||
fileTypes={fileTypes}
|
||||
file={assetFile.value ?? undefined}
|
||||
@@ -183,7 +158,7 @@ export const UploadFiles3D = ({
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!uploadTitle.value || !assetFile.value}
|
||||
disabled={!assetFile.value}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoadingDots } from "@storyteller/ui-loading";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Label } from "@storyteller/ui-label";
|
||||
import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal";
|
||||
import { UploadFiles3D } from "./UploadFiles3D";
|
||||
import { initialUploaderState, UploaderState } from "../../../models";
|
||||
import { Select } from "@storyteller/ui-select";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
UploaderStates,
|
||||
OBJECT_FILE_TYPE,
|
||||
CHARACTER_MIXAMO_FILE_TYPE,
|
||||
IMAGEPLANE_FILE_TYPE,
|
||||
} from "../../../enums";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
galleryModalVisibleViewMode,
|
||||
galleryModalVisibleDuringDrag,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -26,63 +26,17 @@ interface Props {
|
||||
hasLength?: boolean;
|
||||
hasThumbnailUpload?: boolean;
|
||||
};
|
||||
preselectedCategory?: FilterEngineCategories;
|
||||
isSelectVisible: boolean;
|
||||
}
|
||||
|
||||
// Map categories to their allowed file types
|
||||
const categoryFileTypes: Partial<Record<FilterEngineCategories, string[]>> = {
|
||||
[FilterEngineCategories.OBJECT]: Object.values(OBJECT_FILE_TYPE),
|
||||
[FilterEngineCategories.CHARACTER]: [
|
||||
...Object.values(CHARACTER_MIXAMO_FILE_TYPE),
|
||||
],
|
||||
[FilterEngineCategories.IMAGE_PLANE]: Object.values(IMAGEPLANE_FILE_TYPE),
|
||||
[FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE),
|
||||
[FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE),
|
||||
};
|
||||
// Excluded category options for the upload modal
|
||||
const categoryOptions = Object.entries(FilterEngineCategories)
|
||||
.filter(
|
||||
([, value]) =>
|
||||
![
|
||||
"audio",
|
||||
"video_plane",
|
||||
"skybox",
|
||||
"set_dressing",
|
||||
"expression",
|
||||
"animation",
|
||||
"image_plane",
|
||||
"scene",
|
||||
].includes(value),
|
||||
)
|
||||
.sort(([, a], [, b]) => (a === "object" ? -1 : b === "object" ? 1 : 0))
|
||||
.map(([key, value]) => ({
|
||||
// This is because sets is location i guess - BFlat
|
||||
label:
|
||||
key === "LOCATION"
|
||||
? "Set"
|
||||
: key.charAt(0) + key.slice(1).toLowerCase().replace(/_/g, " "),
|
||||
value: value,
|
||||
}));
|
||||
const objectFileTypes = Object.values(OBJECT_FILE_TYPE);
|
||||
|
||||
export function UploadModal3D({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
title,
|
||||
titleIcon,
|
||||
options,
|
||||
preselectedCategory,
|
||||
isSelectVisible,
|
||||
}: Props & { preselectedCategory?: FilterEngineCategories }) {
|
||||
export function UploadModal3D(props: Props) {
|
||||
const { isOpen, onClose, onSuccess, title, titleIcon, options } = props;
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
useState<FilterEngineCategories>(
|
||||
preselectedCategory !== undefined
|
||||
? preselectedCategory
|
||||
: FilterEngineCategories.OBJECT,
|
||||
);
|
||||
|
||||
// Category fixed to OBJECT
|
||||
const selectedCategory = FilterEngineCategories.OBJECT;
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
@@ -98,19 +52,12 @@ export function UploadModal3D({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (preselectedCategory !== undefined) {
|
||||
setSelectedCategory(preselectedCategory);
|
||||
}
|
||||
}, [preselectedCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Preselected category:", preselectedCategory);
|
||||
console.log("Initial selected category:", selectedCategory);
|
||||
}, [preselectedCategory, selectedCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploaderState.status === UploaderStates.success) {
|
||||
// Automatically open the global Gallery modal after a successful upload
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
|
||||
onSuccess(selectedCategory);
|
||||
onClose();
|
||||
}
|
||||
@@ -122,31 +69,10 @@ export function UploadModal3D({
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isSelectVisible && (
|
||||
<div className="flex w-full flex-col">
|
||||
<Label className="mb-1">Category</Label>
|
||||
<Select
|
||||
id="category-select"
|
||||
options={categoryOptions}
|
||||
value={selectedCategory}
|
||||
onChange={(value) => {
|
||||
if (
|
||||
typeof value === "string" &&
|
||||
Object.values(FilterEngineCategories).includes(
|
||||
value as FilterEngineCategories,
|
||||
)
|
||||
) {
|
||||
setSelectedCategory(value as FilterEngineCategories);
|
||||
}
|
||||
}}
|
||||
placeholder="Select a category"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<UploadFiles3D
|
||||
title={title}
|
||||
engineCategory={selectedCategory}
|
||||
fileTypes={categoryFileTypes[selectedCategory] || []}
|
||||
fileTypes={objectFileTypes}
|
||||
options={options}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { FileUploader } from "@storyteller/ui-file-uploader";
|
||||
import { uploadImage } from "./utilities/uploadImage";
|
||||
import { UploaderState } from "../../../models";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
onClose: () => void;
|
||||
onUploadProgress: (newState: UploaderState) => void;
|
||||
}
|
||||
|
||||
export const UploadFilesImage = ({
|
||||
fileTypes,
|
||||
onClose,
|
||||
onUploadProgress,
|
||||
}: Props) => {
|
||||
const [assetFile, setAssetFile] = useState<{
|
||||
value: File | null;
|
||||
error?: string;
|
||||
}>({ value: null });
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!assetFile.value) {
|
||||
setAssetFile((curr) => ({
|
||||
...curr,
|
||||
error: "Please select an image to upload.",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the filename as the title
|
||||
const title = assetFile.value.name.split(".")[0];
|
||||
|
||||
uploadImage({
|
||||
title: title,
|
||||
assetFile: assetFile.value,
|
||||
progressCallback: onUploadProgress,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<FileUploader
|
||||
fileTypes={fileTypes}
|
||||
file={assetFile.value ?? undefined}
|
||||
handleChange={(file: File) => {
|
||||
setAssetFile({
|
||||
value: file,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{assetFile.error && (
|
||||
<h6 className="z-10 text-red">{assetFile.error}</h6>
|
||||
)}
|
||||
|
||||
{assetFile.value && (
|
||||
<div className="relative m-auto flex aspect-square w-full items-center justify-center overflow-hidden rounded-lg bg-brand-secondary">
|
||||
<img
|
||||
alt="Preview"
|
||||
className="m-auto max-h-full max-w-full object-contain"
|
||||
src={URL.createObjectURL(assetFile.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoadingDots } from "@storyteller/ui-loading";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal";
|
||||
import { UploadFilesImage } from "./UploadFilesImage";
|
||||
import { initialUploaderState, UploaderState } from "../../../models";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
UploaderStates,
|
||||
IMAGEPLANE_FILE_TYPE,
|
||||
} from "../../../enums";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
galleryModalVisibleViewMode,
|
||||
galleryModalVisibleDuringDrag,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSuccess: (category: FilterEngineCategories) => void;
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
titleIcon: IconDefinition;
|
||||
}
|
||||
|
||||
const imageFileTypes = Object.values(IMAGEPLANE_FILE_TYPE);
|
||||
|
||||
export function UploadModalImage(props: Props) {
|
||||
const { isOpen, onClose, onSuccess, title, titleIcon } = props;
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
|
||||
// Category fixed to IMAGE_PLANE
|
||||
const selectedCategory = FilterEngineCategories.IMAGE_PLANE;
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
};
|
||||
|
||||
const resetModalState = () => {
|
||||
setUploaderState(initialUploaderState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
resetModalState();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploaderState.status === UploaderStates.success) {
|
||||
// Automatically open the global Gallery modal after a successful upload
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
|
||||
onSuccess(selectedCategory);
|
||||
onClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [uploaderState.status]);
|
||||
|
||||
const UploaderModalContent = () => {
|
||||
switch (uploaderState.status) {
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<UploadFilesImage
|
||||
title={title}
|
||||
fileTypes={imageFileTypes}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case UploaderStates.uploadingAsset:
|
||||
case UploaderStates.uploadingCover:
|
||||
case UploaderStates.settingCover:
|
||||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderStates.success: {
|
||||
return (
|
||||
<UploadSuccess
|
||||
title="Image"
|
||||
onOk={() => {
|
||||
onClose();
|
||||
onSuccess(selectedCategory);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case UploaderStates.assetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={selectedCategory}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
case UploaderStates.coverCreateError:
|
||||
case UploaderStates.coverSetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={"Thumbnail"}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
titleIcon={titleIcon}
|
||||
title={title}
|
||||
className="max-w-xl"
|
||||
showClose={true}
|
||||
>
|
||||
<UploaderModalContent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { UploadModalImage } from "./UploadModalImage";
|
||||
@@ -0,0 +1,56 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { MediaUploadApi } from "../../../../Classes/ApiManager";
|
||||
import { UploaderStates } from "../../../../enums";
|
||||
import { UploaderState } from "../../../../models";
|
||||
import { getFileName } from "../../../../utilities";
|
||||
import { EIntermediateFile } from "~/enums/EIntermediateFile";
|
||||
|
||||
export const uploadImage = async ({
|
||||
title,
|
||||
assetFile,
|
||||
progressCallback,
|
||||
}: {
|
||||
title: string;
|
||||
assetFile: File;
|
||||
progressCallback: (newState: UploaderState) => void;
|
||||
}) => {
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
|
||||
console.log("Uploading image plane:", assetFile);
|
||||
|
||||
progressCallback({ status: UploaderStates.uploadingAsset });
|
||||
|
||||
const assetResponse = await mediaUploadApi.UploadImage({
|
||||
blob: assetFile,
|
||||
fileName: getFileName(assetFile),
|
||||
uuid: uuidv4(),
|
||||
maybe_title: title,
|
||||
is_intermediate_system_file: EIntermediateFile.false,
|
||||
});
|
||||
|
||||
console.log("Image plane response:", assetResponse);
|
||||
|
||||
if (assetResponse == undefined) {
|
||||
console.log("Error: Could not upload image plane!");
|
||||
progressCallback({
|
||||
status: UploaderStates.assetError,
|
||||
errorMessage: "Could not upload image plane!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assetResponse.success || !assetResponse.data) {
|
||||
console.log("Error:", assetResponse.errorMessage);
|
||||
progressCallback({
|
||||
status: UploaderStates.assetError,
|
||||
errorMessage: assetResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Upload successful:", assetResponse.data);
|
||||
progressCallback({
|
||||
status: UploaderStates.success,
|
||||
data: assetResponse.data,
|
||||
});
|
||||
};
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
} from "~/pages/PageEnigma/Editor/editor";
|
||||
// import { usePricingModalStore } from "@storyteller/ui-pricing-modal"; - Uncomment for pricing modal - BFlat
|
||||
import toast from "react-hot-toast";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
interface Props {
|
||||
pageName: string;
|
||||
loginSignUpPressed: () => void;
|
||||
@@ -84,6 +86,7 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
const handleOpenGalleryModal = () => {
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
gtagEvent("open_gallery_modal", { "tab": tabStore.activeTabId });
|
||||
};
|
||||
|
||||
const tabStore = useTabStore();
|
||||
@@ -138,6 +141,8 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
activeMenu={tabStore.activeTabId}
|
||||
disabled={disableTabSwitcher()}
|
||||
onMenuChange={(tabId) => {
|
||||
gtagEvent("switch_tab", { "tab": tabId });
|
||||
|
||||
// Prevent a second input if the switcher is throttled.
|
||||
if (switcherThrottle.current) {
|
||||
return;
|
||||
@@ -193,7 +198,10 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
variant="secondary"
|
||||
icon={faGear}
|
||||
className="h-[38px] w-[38px]"
|
||||
onClick={() => setIsSettingsModalOpen(true)}
|
||||
onClick={() => {
|
||||
setIsSettingsModalOpen(true);
|
||||
gtagEvent("open_settings_modal");
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
|
||||
@@ -14,7 +14,20 @@ export interface JobMaybeResult {
|
||||
entity_type: string;
|
||||
maybe_public_bucket_media_path: string;
|
||||
maybe_successfully_completed_at: string;
|
||||
media_links?: JobMediaLinks;
|
||||
}
|
||||
|
||||
export interface JobMediaLinks {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template?: string;
|
||||
maybe_video_previews?: JobMediaLinksVideoPreviews;
|
||||
}
|
||||
|
||||
export interface JobMediaLinksVideoPreviews {
|
||||
still: string;
|
||||
animated: string;
|
||||
}
|
||||
|
||||
export interface JobRequest {
|
||||
inference_category: string;
|
||||
maybe_model_title: string;
|
||||
|
||||