mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 22:04:51 -05:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9156a7ec6 | ||
|
|
00a9fa9d3c | ||
|
|
204d8377d5 | ||
|
|
480cd43e15 | ||
|
|
c69f9c781c | ||
|
|
324a11bcbe | ||
|
|
d5030f96ba | ||
|
|
0ea24480db | ||
|
|
3d3a8d9a66 | ||
|
|
5d8aac6992 | ||
|
|
95a6ae4746 | ||
|
|
b2fe3052dc |
+12
@@ -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_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "01a0bf33e8331c19dc73f064c54eaa863bec158ef7713450a06c75e246f5f08c"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -4,17 +4,43 @@
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
-- We don't use this.
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
-- TaskStatus enum
|
||||
-- This is how the job system manages job states.
|
||||
-- e.g. pending, started, complete_success, dead, etc.
|
||||
task_status TEXT NOT NULL,
|
||||
|
||||
-- TaskType enum
|
||||
-- e.g. image_generation, video_generation, etc.
|
||||
task_type TEXT NOT NULL,
|
||||
|
||||
-- TaskModelType enum
|
||||
-- e.g. flux_1_dev, veo_2, etc.
|
||||
model_type TEXT,
|
||||
|
||||
-- GenerationProvider enum
|
||||
-- Together with `provider_job_id`, this creates a foreign key lookup to the job.
|
||||
-- e.g. 'artcraft', 'fal', 'sora', etc.
|
||||
provider TEXT,
|
||||
|
||||
-- The primary key for the job in the provider's system.
|
||||
provider_job_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- Tell the job system which caller initiated the task.
|
||||
-- e.g. 'canvas'
|
||||
frontend_caller TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An arbitrary opaque identifier that frontend can set.
|
||||
frontend_subscriber_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An opaque JSON payload set by the frontend subscriber that
|
||||
-- will be re-emitted back to the frontend.
|
||||
frontend_subscriber_payload TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
|
||||
+22
-1
@@ -43,6 +43,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -55,6 +56,21 @@ pub struct EnqueueImageBgRemovalCommand {
|
||||
|
||||
/// Base64-encoded image
|
||||
pub base64_image: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined identifier that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined payload that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -169,7 +185,12 @@ pub async fn handle_request(
|
||||
).await?;
|
||||
|
||||
let result = success_event
|
||||
.insert_into_task_database(task_database)
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref()
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
|
||||
+22
-1
@@ -30,6 +30,7 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use errors::AnyhowError;
|
||||
use log::{error, info, warn};
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
@@ -80,6 +81,21 @@ pub struct EnqueueInpaintImageCommand {
|
||||
|
||||
/// If true, force the dimensions of the source image and mask image to match.
|
||||
pub require_matching_dimensions: Option<bool>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined identifier that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined payload that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -216,7 +232,12 @@ pub async fn handle_request(
|
||||
};
|
||||
|
||||
let result = success_event
|
||||
.insert_into_task_database(task_database)
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref()
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use sqlite_tasks::error::SqliteTasksError;
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
@@ -35,6 +36,22 @@ impl TaskEnqueueSuccess{
|
||||
}
|
||||
|
||||
pub async fn insert_into_task_database(&self, task_database: &TaskDatabase) -> Result<TaskId, SqliteTasksError> {
|
||||
self.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).await
|
||||
}
|
||||
|
||||
// TODO: This belongs somewhere else, not as a method of an event struct.
|
||||
pub async fn insert_into_task_database_with_frontend_payload(
|
||||
&self,
|
||||
task_database: &TaskDatabase,
|
||||
frontend_caller: Option<TauriCommandCaller>,
|
||||
frontend_subscriber_id: Option<&str>,
|
||||
frontend_subscriber_payload: Option<&str>,
|
||||
) -> Result<TaskId, SqliteTasksError> {
|
||||
// TODO: Move this mapping elsewhere, or remove the other models.
|
||||
let model_type = match self.model {
|
||||
None => None,
|
||||
@@ -63,23 +80,12 @@ impl TaskEnqueueSuccess{
|
||||
db: task_database.get_connection(),
|
||||
status: TaskStatus::Pending,
|
||||
task_type: self.task_type,
|
||||
model_type: model_type,
|
||||
model_type,
|
||||
provider: self.provider,
|
||||
provider_job_id: self.provider_job_id.as_deref(),
|
||||
frontend_subscriber_id: None,
|
||||
frontend_subscriber_payload: None,
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload,
|
||||
}).await
|
||||
}
|
||||
|
||||
// pub fn tauri_event_model(&self) -> GenerationModel {
|
||||
// match self.model {
|
||||
// ImageModel::Flux1Dev => GenerationModel::Flux1Dev,
|
||||
// ImageModel::Flux1Schnell => GenerationModel::Flux1Schnell,
|
||||
// ImageModel::FluxPro11 => GenerationModel::FluxPro11,
|
||||
// ImageModel::FluxPro11Ultra => GenerationModel::FluxPro11Ultra,
|
||||
// ImageModel::GptImage1 => GenerationModel::GptImage1,
|
||||
// ImageModel::Recraft3 => GenerationModel::Recraft3,
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::core::events::sendable_event_error::SendableEventError;
|
||||
use enums::tauri::ux::tauri_event_name::TauriEventName;
|
||||
use log::info;
|
||||
use serde::Serialize;
|
||||
use std::fmt::Debug;
|
||||
@@ -29,7 +30,7 @@ struct BasicEventWrapper<T: Serialize + Debug> {
|
||||
|
||||
pub trait BasicSendableEvent : Clone + Debug + Serialize {
|
||||
/// This is the name of the event that the frontend subscribes to.
|
||||
const FRONTEND_EVENT_NAME: &'static str;
|
||||
const FRONTEND_EVENT_NAME: TauriEventName;
|
||||
|
||||
/// This is the status of the event: Success, Failure, etc.
|
||||
const EVENT_STATUS: BasicEventStatus;
|
||||
@@ -41,8 +42,8 @@ pub trait BasicSendableEvent : Clone + Debug + Serialize {
|
||||
status: Self::EVENT_STATUS,
|
||||
data: self.clone(),
|
||||
};
|
||||
info!("Sending event to frontend: {} ; payload = {:?}", Self::FRONTEND_EVENT_NAME, wrapped);
|
||||
let result = app.emit(Self::FRONTEND_EVENT_NAME, wrapped);
|
||||
info!("Sending event to frontend: {} ; payload = {:?}", Self::FRONTEND_EVENT_NAME.to_str(), wrapped);
|
||||
let result = app.emit(Self::FRONTEND_EVENT_NAME.to_str(), wrapped);
|
||||
if let Err(err) = result {
|
||||
return Err(SendableEventError::from(err));
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
|
||||
use enums::tauri::ux::tauri_event_name::TauriEventName;
|
||||
use serde_derive::Serialize;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CanvasBackgroundRemovalCompleteEvent {
|
||||
pub media_token: MediaFileToken,
|
||||
pub image_cdn_url: Url,
|
||||
pub maybe_frontend_subscriber_id: Option<String>,
|
||||
pub maybe_frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
impl BasicSendableEvent for CanvasBackgroundRemovalCompleteEvent {
|
||||
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::CanvasBgRemovedEvent;
|
||||
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod canvas_background_removal_complete_event;
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use enums::tauri::ux::tauri_event_name::TauriEventName;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -11,6 +12,6 @@ pub struct GenerationCompleteEvent {
|
||||
}
|
||||
|
||||
impl BasicSendableEvent for GenerationCompleteEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "generation-complete-event";
|
||||
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::GenerationCompleteEvent;
|
||||
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use enums::tauri::ux::tauri_event_name::TauriEventName;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -14,7 +15,7 @@ pub struct GenerationEnqueueFailureEvent {
|
||||
}
|
||||
|
||||
impl BasicSendableEvent for GenerationEnqueueFailureEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "generation-enqueue-failure-event";
|
||||
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::GenerationEnqueueFailureEvent;
|
||||
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Failure;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use enums::tauri::ux::tauri_event_name::TauriEventName;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -11,6 +12,6 @@ pub struct GenerationEnqueueSuccessEvent {
|
||||
}
|
||||
|
||||
impl BasicSendableEvent for GenerationEnqueueSuccessEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "generation-enqueue-success-event";
|
||||
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::GenerationEnqueueSuccessEvent;
|
||||
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use enums::tauri::ux::tauri_event_name::TauriEventName;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -14,6 +15,6 @@ pub struct GenerationFailedEvent {
|
||||
}
|
||||
|
||||
impl BasicSendableEvent for GenerationFailedEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "generation-failed-event";
|
||||
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::GenerationFailedEvent;
|
||||
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Failure;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod basic_sendable_event_trait;
|
||||
pub mod generation_events;
|
||||
pub mod sendable_event_error;
|
||||
pub mod sendable_event_trait;
|
||||
pub mod sendable_event_trait;
|
||||
pub mod functional_events;
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
use crate::core::artcraft_error::ArtcraftError;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
|
||||
use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent;
|
||||
use artcraft_api_defs::jobs::list_session_jobs::ListSessionJobsItem;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use errors::AnyhowResult;
|
||||
use log::error;
|
||||
use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub async fn maybe_send_background_removal_complete_event(
|
||||
app: &tauri::AppHandle,
|
||||
task: &Task,
|
||||
job: &ListSessionJobsItem,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
match task.task_type {
|
||||
TaskType::BackgroundRemoval => {} // NB: Fall-through
|
||||
_ => return Ok(()),
|
||||
}
|
||||
|
||||
match task.frontend_caller {
|
||||
Some(TauriCommandCaller::Canvas) => {} // NB: Fall-through
|
||||
_ => return Ok(()),
|
||||
}
|
||||
|
||||
let result = match job.maybe_result {
|
||||
Some(ref res) => res,
|
||||
None => {
|
||||
error!("Job result is None for task: {:?}", task);
|
||||
return Ok(()); // No result, nothing to do
|
||||
},
|
||||
};
|
||||
|
||||
let event = CanvasBackgroundRemovalCompleteEvent {
|
||||
media_token: MediaFileToken::new_from_str(&result.entity_token),
|
||||
image_cdn_url: result.media_links.cdn_url.clone(),
|
||||
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
|
||||
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to send CanvasBackgroundRemovalCompleteEvent: {:?}", err); // Fail open
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod maybe_send_background_removal_complete_event;
|
||||
@@ -1 +1,2 @@
|
||||
pub mod events;
|
||||
pub mod storyteller_task_polling_thread;
|
||||
+13
-1
@@ -12,10 +12,12 @@ use crate::core::utils::enum_conversion::task_type::to_generation_action;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::services::storyteller::threads::events::maybe_send_background_removal_complete_event::maybe_send_background_removal_complete_event;
|
||||
use anyhow::anyhow;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use reqwest::Url;
|
||||
@@ -137,7 +139,17 @@ async fn polling_loop(
|
||||
if !updated {
|
||||
continue; // If anything breaks with queries, don't spam events.
|
||||
}
|
||||
|
||||
|
||||
let result = maybe_send_background_removal_complete_event(
|
||||
&app_handle,
|
||||
&task,
|
||||
&job,
|
||||
).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Failed to send background removal complete event: {:?}", err);
|
||||
}
|
||||
|
||||
let service = to_generation_service_provider(task.provider);
|
||||
let action = to_generation_action(task.task_type);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use sqlx::migrate::MigrateError;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
use std::path::Path;
|
||||
@@ -10,19 +12,20 @@ pub struct TaskDbConnection {
|
||||
|
||||
impl TaskDbConnection {
|
||||
pub async fn connect_and_migrate<P: AsRef<Path>>(database_file: P) -> AnyhowResult<Self> {
|
||||
let connection_options = SqliteConnectOptions::new()
|
||||
.filename(database_file)
|
||||
.create_if_missing(true)
|
||||
.journal_mode(SqliteJournalMode::Wal);
|
||||
match run_migrations(&database_file).await {
|
||||
Ok(pool) => return Ok(Self { pool }),
|
||||
Err(err) => {
|
||||
error!("Error running SQLite migrations: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let pool = SqlitePool::connect_with(connection_options).await?;
|
||||
info!("Deleting and recreating SQLite database at {:?}", database_file.as_ref());
|
||||
|
||||
// Run migrations regardless of whether the database is new, SQLx will track which migrations
|
||||
// have been run.
|
||||
// The migrations text get compiled into the binary, so no worries about build inclusion.
|
||||
// Since the task database is being treated as ephemeral, we can always run migrations without
|
||||
// worrying about previous state if we simply blow away old versions of the schema.
|
||||
sqlx::migrate!("../../../../_sql/artcraft_migrations").run(&pool).await?;
|
||||
if let Err(err) = std::fs::remove_file(&database_file) {
|
||||
error!("Error deleting SQLite database file: {:?}", err);
|
||||
}
|
||||
|
||||
let pool = run_migrations(database_file).await?;
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
@@ -31,3 +34,21 @@ impl TaskDbConnection {
|
||||
&self.pool
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_migrations<P: AsRef<Path>>(database_file: P) -> Result<SqlitePool, MigrateError> {
|
||||
let connection_options = SqliteConnectOptions::new()
|
||||
.filename(database_file)
|
||||
.create_if_missing(true)
|
||||
.journal_mode(SqliteJournalMode::Wal);
|
||||
|
||||
let pool = SqlitePool::connect_with(connection_options).await?;
|
||||
|
||||
// Run migrations regardless of whether the database is new, SQLx will track which migrations
|
||||
// have been run.
|
||||
// The migrations text get compiled into the binary, so no worries about build inclusion.
|
||||
// Since the task database is being treated as ephemeral, we can always run migrations without
|
||||
// worrying about previous state if we simply blow away old versions of the schema.
|
||||
sqlx::migrate!("../../../../_sql/artcraft_migrations").run(&pool).await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
pub struct CreateTaskArgs<'a> {
|
||||
@@ -13,6 +14,7 @@ pub struct CreateTaskArgs<'a> {
|
||||
pub model_type: Option<TaskModelType>,
|
||||
pub provider: GenerationProvider,
|
||||
pub provider_job_id: Option<&'a str>,
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
pub frontend_subscriber_id: Option<&'a str>,
|
||||
pub frontend_subscriber_payload: Option<&'a str>,
|
||||
}
|
||||
@@ -28,6 +30,7 @@ pub async fn create_task(
|
||||
let task_type_temp = args.task_type.to_str();
|
||||
let model_type_temp = args.model_type.map(|s| s.to_str());
|
||||
let provider_temp = args.provider.to_string();
|
||||
let frontend_caller_temp = args.frontend_caller.map(|s| s.to_str());
|
||||
|
||||
let query = sqlx::query!(r#"
|
||||
INSERT INTO tasks (
|
||||
@@ -37,10 +40,11 @@ pub async fn create_task(
|
||||
model_type,
|
||||
provider,
|
||||
provider_job_id,
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
task_id_temp,
|
||||
status_temp,
|
||||
@@ -48,6 +52,7 @@ pub async fn create_task(
|
||||
model_type_temp,
|
||||
provider_temp,
|
||||
args.provider_job_id,
|
||||
frontend_caller_temp,
|
||||
args.frontend_subscriber_id,
|
||||
args.frontend_subscriber_payload
|
||||
);
|
||||
|
||||
+14
-4
@@ -4,6 +4,7 @@ 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 enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use sqlx::{QueryBuilder, Sqlite};
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
@@ -22,9 +23,10 @@ pub struct Task {
|
||||
pub id: TaskId,
|
||||
pub status: TaskStatus,
|
||||
pub task_type: TaskType,
|
||||
pub model_type: TaskModelType,
|
||||
pub model_type: Option<TaskModelType>,
|
||||
pub provider: GenerationProvider,
|
||||
pub provider_job_id: Option<String>,
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
@@ -35,9 +37,10 @@ struct RawTask {
|
||||
id: String,
|
||||
task_status: String,
|
||||
task_type: String,
|
||||
model_type: String,
|
||||
model_type: Option<String>,
|
||||
provider: String,
|
||||
provider_job_id: Option<String>,
|
||||
frontend_caller: Option<String>,
|
||||
frontend_subscriber_id: Option<String>,
|
||||
frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
@@ -54,6 +57,7 @@ pub async fn list_tasks_by_provider_and_tokens(
|
||||
model_type,
|
||||
provider,
|
||||
provider_job_id,
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
FROM tasks
|
||||
@@ -91,11 +95,17 @@ 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))?,
|
||||
model_type: task.model_type
|
||||
.map(|model| TaskModelType::from_str(&model)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err)))
|
||||
.transpose()?,
|
||||
provider: GenerationProvider::from_str(&task.provider)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
provider_job_id: task.provider_job_id,
|
||||
frontend_caller: task.frontend_caller
|
||||
.map(|caller| TauriCommandCaller::from_str(&caller)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err)))
|
||||
.transpose()?,
|
||||
frontend_subscriber_id: task.frontend_subscriber_id,
|
||||
frontend_subscriber_payload: task.frontend_subscriber_payload,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ impl GenerationProvider {
|
||||
"artcraft" => Ok(Self::Artcraft),
|
||||
"fal" => Ok(Self::Fal),
|
||||
"sora" => Ok(Self::Sora),
|
||||
_ => Err(format!("invalid task_state: {:?}", job_status)),
|
||||
_ => Err(format!("invalid generation_provider: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ impl ModelType {
|
||||
"hunyuan_3d_2p0" => Ok(Self::Hunyuan3d2_0),
|
||||
"hunyuan_3d_2p1" => Ok(Self::Hunyuan3d2_1),
|
||||
|
||||
_ => Err(format!("invalid task_state: {:?}", job_status)),
|
||||
_ => Err(format!("invalid model_type: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod tasks;
|
||||
pub mod tasks;
|
||||
pub mod ux;
|
||||
@@ -98,7 +98,7 @@ impl TaskModelType {
|
||||
// 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)),
|
||||
_ => Err(format!("invalid task_model_type: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ impl TaskStatus {
|
||||
"cancelled_by_user" => Ok(Self::CancelledByUser),
|
||||
"cancelled_by_provider" => Ok(Self::CancelledByProvider),
|
||||
"cancelled_by_us" => Ok(Self::CancelledByUs),
|
||||
_ => Err(format!("invalid task_state: {:?}", job_status)),
|
||||
_ => Err(format!("invalid task_status: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ impl TaskType {
|
||||
"video_generation" => Ok(Self::VideoGeneration),
|
||||
"object_generation" => Ok(Self::ObjectGeneration),
|
||||
"background_removal" => Ok(Self::BackgroundRemoval),
|
||||
_ => Err(format!("invalid task_state: {:?}", job_status)),
|
||||
_ => Err(format!("invalid task_type: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod tauri_command_caller;
|
||||
pub mod tauri_event_name;
|
||||
@@ -0,0 +1,109 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Defines the names of the Tauri-sent events that the frontend subscribes to.
|
||||
/// These event names are also stored in the database, so keep them short-ish.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TauriCommandCaller {
|
||||
/// The 2D canvas
|
||||
Canvas,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(TauriCommandCaller);
|
||||
impl_mysql_enum_coders!(TauriCommandCaller);
|
||||
impl_mysql_from_row!(TauriCommandCaller);
|
||||
|
||||
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
|
||||
|
||||
impl TauriCommandCaller {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Canvas => "canvas",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(job_status: &str) -> Result<Self, String> {
|
||||
match job_status {
|
||||
"canvas" => Ok(Self::Canvas),
|
||||
_ => Err(format!("invalid tauri_command_caller: {:?}", 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([
|
||||
Self::Canvas,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_helpers::assert_serialization;
|
||||
use crate::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(TauriCommandCaller::Canvas, "canvas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(TauriCommandCaller::Canvas.to_str(), "canvas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(TauriCommandCaller::from_str("canvas").unwrap(), TauriCommandCaller::Canvas);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = TauriCommandCaller::all_variants();
|
||||
assert_eq!(variants.len(), 1);
|
||||
assert_eq!(variants.pop_first(), Some(TauriCommandCaller::Canvas));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
assert_eq!(TauriCommandCaller::all_variants().len(), TauriCommandCaller::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in TauriCommandCaller::all_variants() {
|
||||
// Test to_str(), from_str(), Display, and Debug.
|
||||
assert_eq!(variant, TauriCommandCaller::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, TauriCommandCaller::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, TauriCommandCaller::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
//#[test]
|
||||
//fn serialized_length_ok_for_database() {
|
||||
// const MAX_LENGTH : usize = 16;
|
||||
// for variant in TauriCommandCaller::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);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Defines the names of the Tauri-sent events that the frontend subscribes to.
|
||||
/// These event names are also stored in the database, so keep them short-ish.
|
||||
///
|
||||
/// NB: Events should end in "_event" so they're easy to grep for in Javascript.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TauriEventName {
|
||||
// TODO: Get rid of kebab case.
|
||||
/// General purpose event:
|
||||
/// Generation enqueued
|
||||
#[serde(rename = "generation-enqueue-success-event")]
|
||||
GenerationEnqueueSuccessEvent,
|
||||
|
||||
// TODO: Get rid of kebab case.
|
||||
/// General purpose event:
|
||||
/// Generation failed to enqueue
|
||||
#[serde(rename = "generation-enqueue-failure-event")]
|
||||
GenerationEnqueueFailureEvent,
|
||||
|
||||
// TODO: Get rid of kebab case.
|
||||
/// General purpose event:
|
||||
/// Generation completed successfully
|
||||
#[serde(rename = "generation-complete-event")]
|
||||
GenerationCompleteEvent,
|
||||
|
||||
// TODO: Get rid of kebab case.
|
||||
/// General purpose event:
|
||||
/// Generation failed
|
||||
#[serde(rename = "generation-failed-event")]
|
||||
GenerationFailedEvent,
|
||||
|
||||
/// Special event:
|
||||
/// Background removal complete
|
||||
#[serde(rename = "canvas_bg_removed_event")]
|
||||
CanvasBgRemovedEvent,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(TauriEventName);
|
||||
impl_mysql_enum_coders!(TauriEventName);
|
||||
impl_mysql_from_row!(TauriEventName);
|
||||
|
||||
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
|
||||
|
||||
impl TauriEventName {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::GenerationEnqueueSuccessEvent => "generation-enqueue-success-event",
|
||||
Self::GenerationEnqueueFailureEvent => "generation-enqueue-failure-event",
|
||||
Self::GenerationCompleteEvent => "generation-complete-event",
|
||||
Self::GenerationFailedEvent => "generation-failed-event",
|
||||
Self::CanvasBgRemovedEvent => "canvas_bg_removed_event",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(job_status: &str) -> Result<Self, String> {
|
||||
match job_status {
|
||||
"generation-enqueue-success-event" => Ok(Self::GenerationEnqueueSuccessEvent),
|
||||
"generation-enqueue-failure-event" => Ok(Self::GenerationEnqueueFailureEvent),
|
||||
"generation-complete-event" => Ok(Self::GenerationCompleteEvent),
|
||||
"generation-failed-event" => Ok(Self::GenerationFailedEvent),
|
||||
"canvas_bg_removed_event" => Ok(Self::CanvasBgRemovedEvent),
|
||||
_ => Err(format!("invalid tauri_event_name: {:?}", 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([
|
||||
Self::GenerationEnqueueSuccessEvent,
|
||||
Self::GenerationEnqueueFailureEvent,
|
||||
Self::GenerationCompleteEvent,
|
||||
Self::GenerationFailedEvent,
|
||||
Self::CanvasBgRemovedEvent,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tauri::ux::tauri_event_name::TauriEventName;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(TauriEventName::GenerationEnqueueSuccessEvent, "generation-enqueue-success-event");
|
||||
assert_serialization(TauriEventName::GenerationEnqueueFailureEvent, "generation-enqueue-failure-event");
|
||||
assert_serialization(TauriEventName::GenerationCompleteEvent, "generation-complete-event");
|
||||
assert_serialization(TauriEventName::GenerationFailedEvent, "generation-failed-event");
|
||||
assert_serialization(TauriEventName::CanvasBgRemovedEvent, "canvas_bg_removed_event");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(TauriEventName::GenerationEnqueueSuccessEvent.to_str(), "generation-enqueue-success-event");
|
||||
assert_eq!(TauriEventName::GenerationEnqueueFailureEvent.to_str(), "generation-enqueue-failure-event");
|
||||
assert_eq!(TauriEventName::GenerationCompleteEvent.to_str(), "generation-complete-event");
|
||||
assert_eq!(TauriEventName::GenerationFailedEvent.to_str(), "generation-failed-event");
|
||||
assert_eq!(TauriEventName::CanvasBgRemovedEvent.to_str(), "canvas_bg_removed_event");
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(TauriEventName::from_str("generation-enqueue-success-event").unwrap(), TauriEventName::GenerationEnqueueSuccessEvent);
|
||||
assert_eq!(TauriEventName::from_str("generation-enqueue-failure-event").unwrap(), TauriEventName::GenerationEnqueueFailureEvent);
|
||||
assert_eq!(TauriEventName::from_str("generation-complete-event").unwrap(), TauriEventName::GenerationCompleteEvent);
|
||||
assert_eq!(TauriEventName::from_str("generation-failed-event").unwrap(), TauriEventName::GenerationFailedEvent);
|
||||
assert_eq!(TauriEventName::from_str("canvas_bg_removed_event").unwrap(), TauriEventName::CanvasBgRemovedEvent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = TauriEventName::all_variants();
|
||||
assert_eq!(variants.len(), 5);
|
||||
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent));
|
||||
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent));
|
||||
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent));
|
||||
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationFailedEvent));
|
||||
assert_eq!(variants.pop_first(), Some(TauriEventName::CanvasBgRemovedEvent));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
assert_eq!(TauriEventName::all_variants().len(), TauriEventName::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in TauriEventName::all_variants() {
|
||||
// Test to_str(), from_str(), Display, and Debug.
|
||||
assert_eq!(variant, TauriEventName::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, TauriEventName::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, TauriEventName::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
//#[test]
|
||||
//fn serialized_length_ok_for_database() {
|
||||
// const MAX_LENGTH : usize = 16;
|
||||
// for variant in TauriEventName::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);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,11 @@ import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap";
|
||||
import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot";
|
||||
import { ContextMenuContainer } from "./components/ui/ContextMenu";
|
||||
import { FalBackgroundRemoval } from "@storyteller/tauri-api";
|
||||
import { EnqueueImageBgRemoval } from "@storyteller/tauri-api";
|
||||
import { getCreatorIcon, IMAGE_MODELS_BY_LABEL, ModelCreator, ModelInfo } from "@storyteller/model-list";
|
||||
import { instructiveImageEditModels, ModelCategory, ModelSelector, useModelSelectorStore } from "@storyteller/ui-model-selector";
|
||||
import { useCanvasBgRemovedEvent } from "@storyteller/tauri-api";
|
||||
|
||||
|
||||
export const DecodeBase64ToImage = async (
|
||||
base64String: string,
|
||||
@@ -66,6 +69,27 @@ const PageDraw = () => {
|
||||
onPaste: store.pasteItems,
|
||||
});
|
||||
|
||||
useCanvasBgRemovedEvent(async (event) => {
|
||||
console.log("Canvas bg removed event received:", event);
|
||||
const nodeId = event.maybe_frontend_subscriber_id;
|
||||
if (!nodeId) {
|
||||
console.error("No node ID received from background removal");
|
||||
return;
|
||||
}
|
||||
// const base64String = response.payload?.base64_bytes as string;
|
||||
// const binaryString = atob(base64String);
|
||||
// const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
|
||||
// const blob = new Blob([bytes], { type: "image/png" });
|
||||
// const file = new File([blob], "generated_image.png", {
|
||||
// type: blob.type,
|
||||
// });
|
||||
store.finishRemoveBackground(
|
||||
nodeId,
|
||||
event.media_token,
|
||||
event.image_cdn_url
|
||||
);
|
||||
});
|
||||
|
||||
// Listen for gallery drag and drop events
|
||||
useEffect(() => {
|
||||
const handleGallery2DDrop = (event: CustomEvent) => {
|
||||
@@ -370,46 +394,7 @@ const PageDraw = () => {
|
||||
store.toggleLock(store.selectedNodeIds);
|
||||
break;
|
||||
case "REMOVE_BACKGROUND":
|
||||
await store.removeBackground(
|
||||
store.selectedNodeIds,
|
||||
async (
|
||||
success: boolean,
|
||||
image_base64: string,
|
||||
message: string,
|
||||
) => {
|
||||
if (!success) {
|
||||
console.error(message);
|
||||
return { success: false };
|
||||
}
|
||||
try {
|
||||
const response = await FalBackgroundRemoval({
|
||||
base64_image: image_base64,
|
||||
});
|
||||
if (
|
||||
response.status !== "success" ||
|
||||
!("payload" in response)
|
||||
) {
|
||||
console.error("Failed to remove background", response);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const base64String = response.payload
|
||||
?.base64_bytes as string;
|
||||
const binaryString = atob(base64String);
|
||||
const bytes = Uint8Array.from(binaryString, (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
const blob = new Blob([bytes], { type: "image/png" });
|
||||
const file = new File([blob], "generated_image.png", {
|
||||
type: blob.type,
|
||||
});
|
||||
return { success: true, file };
|
||||
} catch (error) {
|
||||
console.error("Failed to remove background", error);
|
||||
return { success: false };
|
||||
}
|
||||
},
|
||||
);
|
||||
await store.beginRemoveBackground(store.selectedNodeIds);
|
||||
break;
|
||||
case "BRING_TO_FRONT":
|
||||
store.bringToFront(store.selectedNodeIds);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { Node, NodeType } from "../Node";
|
||||
import { EnqueueImageBgRemoval } from "libs/tauri-api/src/lib/enqueue/EnqueueImageBgRemovalCommand";
|
||||
|
||||
// Add LineNode type
|
||||
export type LineNode = {
|
||||
@@ -231,13 +232,17 @@ interface SceneState {
|
||||
sendToBack: (nodeIds: string[]) => void;
|
||||
bringForward: (nodeIds: string[]) => void;
|
||||
sendBackward: (nodeIds: string[]) => void;
|
||||
removeBackground: (
|
||||
|
||||
// Start background removal (enqueues task with Tauri)
|
||||
beginRemoveBackground: (
|
||||
nodeIds: string[],
|
||||
operation: (
|
||||
success: boolean,
|
||||
base64_image: string,
|
||||
message: string,
|
||||
) => Promise<{ success: boolean; file?: File }>,
|
||||
) => Promise<void>;
|
||||
|
||||
// Finish background removal (handoff back from a Tauri-sent event)
|
||||
finishRemoveBackground: (
|
||||
nodeId: string,
|
||||
mediaToken: string,
|
||||
imageCdnUrl: string,
|
||||
) => Promise<void>;
|
||||
|
||||
// Add new lock action
|
||||
@@ -1432,61 +1437,71 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
removeBackground: async (
|
||||
beginRemoveBackground: async (
|
||||
nodeIds: string[],
|
||||
operation: (
|
||||
success: boolean,
|
||||
base64_image: string,
|
||||
message: string,
|
||||
) => Promise<{ success: boolean; file?: File }>,
|
||||
) => {
|
||||
const hasImageNodes = nodeIds.some((id) => {
|
||||
const node = get().nodes.find((n) => n.id === id);
|
||||
return node ? node.type === "image" : false;
|
||||
});
|
||||
|
||||
if (hasImageNodes) {
|
||||
const firstNode = get().nodes.find(
|
||||
(node) => nodeIds.includes(node.id) && node.type === "image",
|
||||
);
|
||||
if (firstNode && firstNode.imageFile) {
|
||||
try {
|
||||
const base64Image = await convertFileToBase64(firstNode.imageFile);
|
||||
const { success, file } = await operation(
|
||||
true,
|
||||
base64Image,
|
||||
"Removing Background.",
|
||||
);
|
||||
|
||||
if (success && file) {
|
||||
// Create a new node instance
|
||||
const updatedNode = new Node({
|
||||
...firstNode,
|
||||
imageFile: file,
|
||||
});
|
||||
|
||||
// Load the new image
|
||||
await updatedNode.setImageFromFile(file);
|
||||
|
||||
// Update the store with the fully loaded node
|
||||
set((state: SceneState) => ({
|
||||
nodes: state.nodes.map((node: Node) => {
|
||||
if (node.id === firstNode.id) {
|
||||
return updatedNode;
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
}));
|
||||
|
||||
get().saveState();
|
||||
if (!hasImageNodes) {
|
||||
return;
|
||||
}
|
||||
const firstNode = get().nodes.find(
|
||||
(node) => nodeIds.includes(node.id) && node.type === "image",
|
||||
);
|
||||
if (!firstNode || !firstNode.imageFile) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const base64Image = await convertFileToBase64(firstNode.imageFile);
|
||||
// NB: This handler is async, so we subscribe (externally) and
|
||||
// wait for the bg removal to complete.
|
||||
const _response = await EnqueueImageBgRemoval({
|
||||
base64_image: base64Image,
|
||||
frontend_caller: "canvas",
|
||||
frontend_subscriber_id: firstNode.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error starting background removal:", error);
|
||||
}
|
||||
},
|
||||
finishRemoveBackground: async (
|
||||
nodeId: string,
|
||||
mediaToken: string,
|
||||
imageCdnUrl: string,
|
||||
) => {
|
||||
if (!nodeId || !mediaToken || !imageCdnUrl) {
|
||||
return;
|
||||
}
|
||||
const firstNode = get().nodes.find(
|
||||
(node) => node.id === nodeId && node.type === "image",
|
||||
);
|
||||
if (!firstNode || !firstNode.imageFile) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Create a new node instance from the old node
|
||||
const updatedNode = new Node({
|
||||
...firstNode,
|
||||
imageUrl: imageCdnUrl,
|
||||
});
|
||||
// Load the new image
|
||||
await updatedNode.setImageFromUrl(imageCdnUrl);
|
||||
// Update the store with the fully loaded node
|
||||
set((state: SceneState) => ({
|
||||
nodes: state.nodes.map((node: Node) => {
|
||||
if (node.id === firstNode.id) {
|
||||
return updatedNode;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating image:", error);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
}));
|
||||
get().saveState();
|
||||
} catch (error) {
|
||||
console.error("Error completing background removal:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// Add toggleLock action
|
||||
toggleLock: (nodeIds: string[]) => {
|
||||
set((state) => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap"
|
||||
import {
|
||||
EnqueueImageInpaint,
|
||||
EnqueueImageInpaintModel,
|
||||
FalBackgroundRemoval,
|
||||
} from "@storyteller/tauri-api";
|
||||
import { ContextMenuContainer } from "../PageDraw/components/ui/ContextMenu";
|
||||
import SideToolbar from "../PageDraw/components/ui/SideToolbar";
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export * from "./lib/common/CommandStatus.js";
|
||||
export * from "./lib/enqueue/EnqueueContextualEditImage.js";
|
||||
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
|
||||
export * from "./lib/enqueue/EnqueueTextToImage.js";
|
||||
export * from "./lib/enqueue/EnqueueImageToVideo.js";
|
||||
export * from "./lib/enqueue/EnqueueImageBgRemovalCommand.js";
|
||||
export * from "./lib/enqueue/EnqueueImageInpaint.js";
|
||||
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
|
||||
export * from "./lib/enqueue/EnqueueImageToVideo.js";
|
||||
export * from "./lib/enqueue/EnqueueTextToImage.js";
|
||||
export * from "./lib/events/BasicEventWrapper.js";
|
||||
export * from "./lib/events/CanvasBgRemovedEvent.js";
|
||||
export * from "./lib/fal/FalBackgroundRemoval.js";
|
||||
export * from "./lib/fal/FalHunyuanImageTo3d.js";
|
||||
export * from "./lib/fal/FalKlingImageToVideo.js";
|
||||
@@ -11,6 +14,9 @@ export * from "./lib/fal/GetFalApiKey.js";
|
||||
export * from "./lib/fal/SetFalApiKey.js";
|
||||
export * from "./lib/openai/GetOpenAIApiKey.js";
|
||||
export * from "./lib/openai/SetOpenAIApiKey.js";
|
||||
export * from "./lib/providers/GetProviderOrder.js";
|
||||
export * from "./lib/providers/Provider.js";
|
||||
export * from "./lib/providers/SetProviderOrder.js";
|
||||
export * from "./lib/settings/GetAppPreferences.js";
|
||||
export * from "./lib/settings/UpdateAppPreference.js";
|
||||
export * from "./lib/sora/CheckSoraSession.js";
|
||||
@@ -19,7 +25,4 @@ export * from "./lib/sora/SoraImageRemix.js";
|
||||
export * from "./lib/sora/useSoraLoginListener.js";
|
||||
export * from "./lib/sora/waitForSoraLogin.js";
|
||||
export * from "./lib/system/GetAppInfo.js";
|
||||
export * from "./lib/providers/SetProviderOrder.js";
|
||||
export * from "./lib/providers/Provider.js";
|
||||
export * from "./lib/providers/GetProviderOrder.js";
|
||||
export * from "./lib/util/LoadWithoutCors.js";
|
||||
export * from "./lib/util/LoadWithoutCors.js";
|
||||
@@ -0,0 +1,71 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
|
||||
export interface EnqueueImageBgRemovalRequest {
|
||||
image_media_token?: string;
|
||||
base64_image?: string;
|
||||
|
||||
// TODO: Actual enum.
|
||||
frontend_caller?: string;
|
||||
|
||||
// We'll use the node id here.
|
||||
frontend_subscriber_id?: string;
|
||||
}
|
||||
|
||||
interface RawEnqueueImageBgRemovalRequest {
|
||||
image_media_token?: string;
|
||||
base64_image?: string;
|
||||
frontend_caller?: string;
|
||||
frontend_subscriber_id?: string;
|
||||
}
|
||||
|
||||
export enum EnqueueImageBgRemovalErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified = "model_not_specified",
|
||||
/// Generic server error
|
||||
ServerError = "server_error",
|
||||
/// No Fal API key available
|
||||
NeedsFalApiKey = "needs_fal_api_key",
|
||||
/// Fal had an API error
|
||||
FalError = "fal_error",
|
||||
}
|
||||
|
||||
export interface EnqueueImageBgRemovalError extends CommandResult {
|
||||
error_type: EnqueueImageBgRemovalErrorType;
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
export interface EnqueueImageBgRemovalPayload {
|
||||
}
|
||||
|
||||
export interface EnqueueImageBgRemovalSuccess extends CommandResult {
|
||||
payload: EnqueueImageBgRemovalPayload;
|
||||
}
|
||||
|
||||
export type EnqueueImageBgRemovalResult = EnqueueImageBgRemovalSuccess | EnqueueImageBgRemovalError;
|
||||
|
||||
export const EnqueueImageBgRemoval = async (request: EnqueueImageBgRemovalRequest) : Promise<EnqueueImageBgRemovalResult> => {
|
||||
let mutableRequest : RawEnqueueImageBgRemovalRequest = {};
|
||||
|
||||
if (!!request.image_media_token) {
|
||||
mutableRequest.image_media_token = request.image_media_token;
|
||||
}
|
||||
|
||||
if (!!request.base64_image) {
|
||||
mutableRequest.base64_image = request.base64_image;
|
||||
}
|
||||
|
||||
if (!!request.frontend_caller) {
|
||||
mutableRequest.frontend_caller = request.frontend_caller;
|
||||
}
|
||||
|
||||
if (!!request.frontend_subscriber_id) {
|
||||
mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id;
|
||||
}
|
||||
|
||||
const result = await invoke("enqueue_image_bg_removal_command", {
|
||||
request: mutableRequest,
|
||||
});
|
||||
|
||||
return (result as EnqueueImageBgRemovalResult);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
export enum BasicEventStatus {
|
||||
Success = "success",
|
||||
Failure = "failure",
|
||||
};
|
||||
|
||||
export interface BasicEventWrapper<T> {
|
||||
status: BasicEventStatus,
|
||||
data: T,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { BasicEventWrapper } from './BasicEventWrapper';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const EVENT_NAME : string = 'canvas_bg_removed_event';
|
||||
|
||||
export interface CanvasBgRemovedEvent {
|
||||
media_token: string,
|
||||
image_cdn_url: string,
|
||||
maybe_frontend_subscriber_id?: string,
|
||||
maybe_frontend_subscriber_payload?: string,
|
||||
}
|
||||
|
||||
export const useCanvasBgRemovedEvent = (asyncCallback: (event: CanvasBgRemovedEvent) => Promise<void>) => {
|
||||
useEffect(() => {
|
||||
let isUnmounted = false;
|
||||
let unlisten: Promise<UnlistenFn>;
|
||||
|
||||
const setup = async () => {
|
||||
unlisten = listen<BasicEventWrapper<CanvasBgRemovedEvent>>(EVENT_NAME, async (wrappedEvent) => {
|
||||
await asyncCallback(wrappedEvent.payload.data);
|
||||
});
|
||||
|
||||
if (isUnmounted) {
|
||||
unlisten.then(f => f()); // Unsubscribe if unmounted early.
|
||||
}
|
||||
};
|
||||
|
||||
setup();
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
unlisten.then(f => f());
|
||||
};
|
||||
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user