mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de0776009b | ||
|
|
11f8885bf6 | ||
|
|
0e2926b179 | ||
|
|
671e02edcb | ||
|
|
10f8649089 | ||
|
|
3aeea6d2b2 | ||
|
|
abccceb5bd | ||
|
|
9add7ee1cd | ||
|
|
d551e48a23 | ||
|
|
5cb608fad6 | ||
|
|
d5ae28ca4c | ||
|
|
8313558795 | ||
|
|
95173e032f | ||
|
|
b0f6551623 |
+35
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -49,18 +49,43 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"name": "on_complete_primary_media_file_token",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_class",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_batch_token",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_cdn_url",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_thumbnail_url_template",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"ordinal": 14,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"ordinal": 10,
|
||||
"ordinal": 15,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "completed_at: DateTime<Utc>",
|
||||
"ordinal": 11,
|
||||
"ordinal": 16,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
@@ -77,10 +102,15 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a142dc3fdf5b6e741c268d8f2e9996a28c214f5835681297d58a9431da8de939"
|
||||
"hash": "47f94c127abc20683b25c257aed3c2190b9bd29eb829d61392f08e10fd986767"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "50a2449b87abed7df3302a74ecd0e8ab87940077a247636680cb09b2fe488321"
|
||||
}
|
||||
Generated
-3
@@ -660,7 +660,6 @@ dependencies = [
|
||||
"discord-rich-presence",
|
||||
"enums",
|
||||
"errors",
|
||||
"fal_client",
|
||||
"filesys",
|
||||
"futures 0.3.31",
|
||||
"idempotency",
|
||||
@@ -5573,7 +5572,6 @@ dependencies = [
|
||||
"mimetypes",
|
||||
"mysql_queries",
|
||||
"once_cell",
|
||||
"openai_sora_client",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry_sdk",
|
||||
@@ -5586,7 +5584,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_service_components",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"subprocess_common",
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
-- tasks_v3.sqlite - added frontend_caller, comments
|
||||
-- tasks_v4.sqlite - added is_dismissed_by_user
|
||||
-- tasks_v5.sqlite - added five fields:
|
||||
-- on_complete_batch_token,
|
||||
-- on_complete_primary_media_file_token,
|
||||
-- on_complete_primary_media_file_class,
|
||||
-- on_complete_primary_media_file_cdn_url,
|
||||
-- on_complete_primary_media_file_thumbnail_url_template,
|
||||
--
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
@@ -54,6 +61,29 @@ CREATE TABLE tasks (
|
||||
-- Whether the user has dismissed the task from view.
|
||||
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the batch token (if any).
|
||||
-- This will point to a collection of multiple files.
|
||||
on_complete_batch_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the media token of the "primary" media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the file type of the primary media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_class TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL to the media file.
|
||||
on_complete_primary_media_file_cdn_url TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL template for the thumbnail of the primary media file.
|
||||
on_complete_primary_media_file_thumbnail_url_template TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
completed_at INTEGER DEFAULT NULL
|
||||
|
||||
@@ -31,7 +31,6 @@ build_metadata.workspace = true
|
||||
cookie_store.workspace = true
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
fal_client.workspace = true
|
||||
filesys.workspace = true
|
||||
idempotency.workspace = true
|
||||
images.workspace = true
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::core::artcraft_error::ArtcraftError;
|
||||
use base64::DecodeError;
|
||||
use errors::AnyhowError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use midjourney_client::error::midjourney_error::MidjourneyError;
|
||||
use openai_sora_client::error::sora_error::SoraError;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
//use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GenerateError {
|
||||
@@ -14,6 +14,9 @@ pub enum GenerateError {
|
||||
|
||||
/// We couldn't find a provider to dispatch the request to.
|
||||
NoProviderAvailable,
|
||||
|
||||
/// We pulled out Fal (for now) - it's impacting build speeds. We'll add it in the future.
|
||||
FalNoLongerSupported,
|
||||
|
||||
/// There was a billing, credits, or payments issue.
|
||||
BillingIssue(BillingIssueReason),
|
||||
@@ -73,7 +76,7 @@ pub enum BillingProvider {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProviderFailureReason {
|
||||
Fal(FalErrorPlus),
|
||||
//Fal(FalErrorPlus),
|
||||
MidjourneyError(MidjourneyError),
|
||||
/// NB: The midjourney client doesn't categorize all errors, so we have to do so on our end.
|
||||
MidjourneyJobEnqueueFailed,
|
||||
@@ -141,11 +144,11 @@ impl From<ArtcraftError> for GenerateError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FalErrorPlus> for GenerateError {
|
||||
fn from(value: FalErrorPlus) -> Self {
|
||||
Self::ProviderFailure(ProviderFailureReason::Fal(value))
|
||||
}
|
||||
}
|
||||
//impl From<FalErrorPlus> for GenerateError {
|
||||
// fn from(value: FalErrorPlus) -> Self {
|
||||
// Self::ProviderFailure(ProviderFailureReason::Fal(value))
|
||||
// }
|
||||
//}
|
||||
|
||||
impl From<MidjourneyError> for GenerateError {
|
||||
fn from(value: MidjourneyError) -> Self {
|
||||
|
||||
+2
-12
@@ -7,6 +7,7 @@ use crate::core::commands::response::failure_response_wrapper::{CommandErrorResp
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
@@ -18,8 +19,6 @@ use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::simple_http_download::simple_http_download;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -35,12 +34,11 @@ use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::endpoints::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EnqueueImageBgRemovalCommand {
|
||||
@@ -103,8 +101,6 @@ pub async fn enqueue_image_bg_removal_command(
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
) -> ResponseOrErrorType<EnqueueImageBgRemovalSuccessResponse, EnqueueImageBgRemovalErrorType> {
|
||||
|
||||
info!("enqueue_image_bg_removal_command called");
|
||||
@@ -117,8 +113,6 @@ pub async fn enqueue_image_bg_removal_command(
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&storyteller_creds_manager,
|
||||
&fal_creds_manager,
|
||||
&fal_task_queue,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
@@ -167,8 +161,6 @@ pub async fn handle_request(
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
// TODO(bt,2025-07-07): Other model/provider routing...
|
||||
@@ -180,8 +172,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
).await?;
|
||||
|
||||
let result = success_event
|
||||
|
||||
-6
@@ -9,8 +9,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -24,8 +22,6 @@ pub async fn handle_generic_bg_removal(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
@@ -57,8 +53,6 @@ pub async fn handle_generic_bg_removal(
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -9,8 +9,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -23,7 +21,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
|
||||
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
|
||||
-4
@@ -3,8 +3,6 @@ use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_c
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_generic_bg_removal_fal(
|
||||
@@ -12,8 +10,6 @@ pub async fn handle_generic_bg_removal_fal(
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
Err(GenerateError::NotYetImplemented("TODO: Implement fal background removal handling".to_string()))
|
||||
}
|
||||
|
||||
-14
@@ -20,8 +20,6 @@ use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::simple_http_download::simple_http_download;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -169,8 +167,6 @@ pub async fn enqueue_contextual_edit_image_command(
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
) -> ResponseOrErrorType<EnqueueContextualEditImageSuccessResponse, EnqueueContextualEditImageErrorType> {
|
||||
@@ -186,8 +182,6 @@ pub async fn enqueue_contextual_edit_image_command(
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&storyteller_creds_manager,
|
||||
&fal_creds_manager,
|
||||
&fal_task_queue,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
).await;
|
||||
@@ -238,8 +232,6 @@ pub async fn handle_request(
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
@@ -255,8 +247,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
@@ -269,8 +259,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
@@ -283,8 +271,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
|
||||
-4
@@ -7,8 +7,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -22,8 +20,6 @@ pub async fn handle_flux_kontext_edit(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
-4
@@ -7,8 +7,6 @@ use crate::core::events::generation_events::generation_enqueue_failure_event::Ge
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -21,8 +19,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::Gene
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_pro_kontext_max_edit_webhook::FluxProKontextMaxNumImages;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
|
||||
|
||||
-4
@@ -7,8 +7,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -22,8 +20,6 @@ pub async fn handle_gemini_25_flash_edit(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
-2
@@ -7,8 +7,6 @@ use crate::core::events::generation_events::generation_enqueue_failure_event::Ge
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
|
||||
-4
@@ -6,8 +6,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -23,8 +21,6 @@ pub async fn handle_gpt_image_1_edit(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
-4
@@ -8,8 +8,6 @@ use crate::core::events::generation_events::generation_enqueue_failure_event::Ge
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -21,8 +19,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::Gene
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
|
||||
-3
@@ -14,8 +14,6 @@ use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::simple_http_download::simple_http_download;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -27,7 +25,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
|
||||
+1
-17
@@ -22,8 +22,6 @@ use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::simple_http_download::simple_http_download;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -40,8 +38,8 @@ use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::endpoints::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -158,8 +156,6 @@ pub async fn enqueue_image_inpaint_command(
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
) -> ResponseOrErrorType<EnqueueImageInpaintSuccessResponse, EnqueueInpaintImageErrorType> {
|
||||
@@ -175,8 +171,6 @@ pub async fn enqueue_image_inpaint_command(
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&storyteller_creds_manager,
|
||||
&fal_creds_manager,
|
||||
&fal_task_queue,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
).await;
|
||||
@@ -227,8 +221,6 @@ pub async fn handle_request(
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
@@ -244,8 +236,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
@@ -258,8 +248,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
@@ -273,8 +261,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
@@ -288,8 +274,6 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
|
||||
-4
@@ -8,8 +8,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -23,8 +21,6 @@ pub async fn handle_flux_dev_juggernaut_inpaint(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
-3
@@ -10,8 +10,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -24,7 +22,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
|
||||
use artcraft_api_defs::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::{FluxDevJuggernautInpaintImageNumImages, FluxDevJuggernautInpaintImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
|
||||
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
|
||||
|
||||
-4
@@ -8,8 +8,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -23,8 +21,6 @@ pub async fn handle_flux_pro_1_inpaint(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
-3
@@ -10,8 +10,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -24,7 +22,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
|
||||
use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
|
||||
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
|
||||
|
||||
-4
@@ -6,8 +6,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -21,8 +19,6 @@ pub async fn handle_flux_pro_kontext_inpaint(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
-3
@@ -10,8 +10,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -25,7 +23,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
|
||||
use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
|
||||
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
|
||||
|
||||
-4
@@ -7,8 +7,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -22,8 +20,6 @@ pub async fn handle_gemini_25_flash_inpaint(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
-3
@@ -10,8 +10,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -26,7 +24,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
|
||||
use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
|
||||
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
|
||||
|
||||
-11
@@ -12,11 +12,8 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
@@ -75,8 +72,6 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
) -> Response<EnqueueImageTo3dObjectSuccessResponse, EnqueueImageTo3dObjectErrorType, ()> {
|
||||
@@ -90,9 +85,7 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
@@ -162,9 +155,7 @@ pub async fn handle_request(
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let result = handle_object(
|
||||
@@ -173,9 +164,7 @@ pub async fn handle_request(
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
).await;
|
||||
|
||||
let success_event = match result {
|
||||
|
||||
+2
-17
@@ -1,3 +1,4 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::EnqueueImageTo3dObjectRequest;
|
||||
use crate::core::commands::enqueue::image_to_object::generic::handle_object_artcraft::handle_object_artcraft;
|
||||
use crate::core::commands::enqueue::image_to_object::generic::handle_object_fal::handle_object_fal;
|
||||
@@ -5,11 +6,8 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use tauri::AppHandle;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
|
||||
pub async fn handle_object(
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
@@ -17,15 +15,14 @@ pub async fn handle_object(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {} // Fallthrough
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_object_artcraft(
|
||||
@@ -36,18 +33,6 @@ pub async fn handle_object(
|
||||
storyteller_creds_manager
|
||||
).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_object_fal(
|
||||
&app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
request,
|
||||
fal_creds_manager,
|
||||
fal_task_queue
|
||||
).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -12,7 +12,6 @@ use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::Gener
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::endpoints::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
|
||||
|
||||
+2
-123
@@ -1,21 +1,8 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
|
||||
use crate::core::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::EnqueueImageTo3dObjectRequest;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use anyhow::anyhow;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info};
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_object_fal(
|
||||
@@ -23,114 +10,6 @@ pub async fn handle_object_fal(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let api_key = match fal_creds_manager.get_key()? {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
error!("No FAL API key is set. Can't perform action.");
|
||||
let event =
|
||||
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::ImageTo3d);
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(GenerateError::needs_fal_api_key());
|
||||
},
|
||||
};
|
||||
|
||||
//let api_key = fal_creds_manager.get_key_required()
|
||||
// .map_err(|err| {
|
||||
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
|
||||
// InternalObjectError::NeedsFalApiKey
|
||||
// })?;
|
||||
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else {
|
||||
return Err(GenerateError::required_source_image_not_provided());
|
||||
}
|
||||
|
||||
info!("Calling FAL image to 3d ...");
|
||||
|
||||
let filename = temp_download.path().to_path_buf();
|
||||
|
||||
let mut used_model = None;
|
||||
|
||||
let result = match request.model {
|
||||
None => {
|
||||
return Err(GenerateError::no_model_specified());
|
||||
}
|
||||
Some(
|
||||
EnqueueImageTo3dObjectModel::Hunyuan3d2_0 |
|
||||
EnqueueImageTo3dObjectModel::Hunyuan3d2
|
||||
) => {
|
||||
used_model = Some(GenerationModel::Hunyuan3d2_0);
|
||||
info!("enqueue Hunyuan 3D 2.0");
|
||||
enqueue_hunyuan2_image_to_3d(Hunyuan2Args {
|
||||
image_path: filename,
|
||||
api_key: &api_key,
|
||||
}).await
|
||||
}
|
||||
_ => {
|
||||
return Err(GenerateError::AnyhowError(anyhow!("Wrong model specified: {:?}", request.model)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut job_id = None;
|
||||
|
||||
match result {
|
||||
Ok(enqueued) => {
|
||||
info!("Successfully enqueued text to image");
|
||||
|
||||
job_id = Some(enqueued.request_id.to_string());
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::ImageTo3d,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
if let Err(err) = fal_task_queue.insert(&enqueued) {
|
||||
error!("Failed to enqueue task: {:?}", err);
|
||||
return Err(GenerateError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to enqueue image to 3d: {:?}", err);
|
||||
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::ImageTo3d,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(GenerateError::from(err));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::ObjectGeneration,
|
||||
model: used_model,
|
||||
provider: GenerationProvider::Fal,
|
||||
provider_job_id: job_id,
|
||||
})
|
||||
Err(GenerateError::FalNoLongerSupported)
|
||||
}
|
||||
|
||||
-11
@@ -13,12 +13,9 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
@@ -118,8 +115,6 @@ pub async fn enqueue_image_to_video_command(
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
@@ -134,10 +129,8 @@ pub async fn enqueue_image_to_video_command(
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&fal_creds_manager,
|
||||
&sora_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
@@ -207,10 +200,8 @@ pub async fn handle_request(
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let result = match request.model {
|
||||
@@ -231,9 +222,7 @@ pub async fn handle_request(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
provider_priority_store,
|
||||
fal_creds_manager,
|
||||
storyteller_creds_manager,
|
||||
fal_task_queue,
|
||||
).await
|
||||
}
|
||||
};
|
||||
|
||||
+1
-17
@@ -1,13 +1,10 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
|
||||
use crate::core::commands::enqueue::image_to_video::generic::handle_video_artcraft::handle_video_artcraft;
|
||||
use crate::core::commands::enqueue::image_to_video::generic::handle_video_fal::handle_video_fal;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use tauri::AppHandle;
|
||||
|
||||
@@ -17,15 +14,14 @@ pub async fn handle_video(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {} // Fallthrough
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_video_artcraft(
|
||||
@@ -36,18 +32,6 @@ pub async fn handle_video(
|
||||
storyteller_creds_manager
|
||||
).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_video_fal(
|
||||
&app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
request,
|
||||
fal_creds_manager,
|
||||
fal_task_queue
|
||||
).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -18,9 +18,6 @@ use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::Gene
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::{GenerateVeo3AspectRatio, GenerateVeo3ImageToVideoRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::Kling16ProAspectRatio;
|
||||
use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2AspectRatio;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::endpoints::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video;
|
||||
|
||||
+3
-112
@@ -1,21 +1,8 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, VideoModel};
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use anyhow::anyhow;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
|
||||
use log::{error, info};
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_video_fal(
|
||||
@@ -23,102 +10,6 @@ pub async fn handle_video_fal(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let api_key = match fal_creds_manager.get_key()? {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
error!("No FAL API key is set. Can't perform action.");
|
||||
let event =
|
||||
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::GenerateVideo);
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(GenerateError::needs_fal_api_key());
|
||||
},
|
||||
};
|
||||
|
||||
//let api_key = fal_creds_manager.get_key_required()
|
||||
// .map_err(|err| {
|
||||
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
|
||||
// InternalVideoError::NeedsFalApiKey
|
||||
// })?;
|
||||
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else {
|
||||
return Err(GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided));
|
||||
}
|
||||
|
||||
let filename = temp_download.path().to_path_buf();
|
||||
|
||||
let mut selected_model = None;
|
||||
|
||||
let result = match request.model {
|
||||
None => {
|
||||
return Err(GenerateError::no_model_specified());
|
||||
}
|
||||
Some(VideoModel::Kling16Pro) => {
|
||||
info!("enqueue Kling 1.6 image to video with Kling API");
|
||||
selected_model = Some(GenerationModel::Kling1_6);
|
||||
enqueue_kling_16_pro_image_to_video(Kling16ProArgs {
|
||||
image_path: filename,
|
||||
api_key: &api_key,
|
||||
duration: Kling16ProDuration::Default,
|
||||
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
|
||||
prompt: request.prompt.as_deref().unwrap_or(""),
|
||||
}).await
|
||||
}
|
||||
_ => {
|
||||
return Err(GenerateError::AnyhowError(anyhow!("Unsupported model: {:?}", request.model)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut job_id = None;
|
||||
|
||||
match result {
|
||||
Ok(enqueued) => {
|
||||
info!("Successfully enqueued Kling 1.6 image to video");
|
||||
|
||||
job_id = Some(enqueued.request_id.to_string());
|
||||
|
||||
if let Err(err) = fal_task_queue.insert(&enqueued) {
|
||||
error!("Failed to enqueue task: {:?}", err);
|
||||
return Err(GenerateError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to enqueue image to video: {:?}", err);
|
||||
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::GenerateVideo,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: selected_model,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(GenerateError::from(err));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::VideoGeneration,
|
||||
model: selected_model,
|
||||
provider: GenerationProvider::Fal,
|
||||
provider_job_id: job_id,
|
||||
})
|
||||
Err(GenerateError::FalNoLongerSupported)
|
||||
}
|
||||
|
||||
-2
@@ -7,8 +7,6 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
|
||||
+1
-18
@@ -19,8 +19,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
@@ -29,7 +27,6 @@ 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 fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
@@ -146,10 +143,8 @@ pub async fn enqueue_text_to_image_command(
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
mj_creds_manager: State<'_, MidjourneyCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
) -> Response<EnqueueTextToImageSuccessResponse, EnqueueTextToImageErrorType, ()> {
|
||||
@@ -162,11 +157,9 @@ pub async fn enqueue_text_to_image_command(
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&fal_creds_manager,
|
||||
&mj_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&app_env_configs,
|
||||
&fal_task_queue,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
).await;
|
||||
@@ -232,11 +225,9 @@ pub async fn handle_request(
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
mj_creds_manager: &MidjourneyCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
@@ -246,10 +237,8 @@ pub async fn handle_request(
|
||||
&app,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&app_env_configs,
|
||||
&fal_task_queue,
|
||||
&mj_creds_manager,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
@@ -282,10 +271,8 @@ pub async fn dispatch_request(
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
mj_creds_manager: &MidjourneyCredentialManager,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
@@ -336,16 +323,12 @@ pub async fn dispatch_request(
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {} // Fallthrough
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return handle_image_artcraft(
|
||||
request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await;
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -17,7 +17,6 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
|
||||
-1
@@ -15,7 +15,6 @@ use artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemin
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{Gen
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages, GenerateFluxPro11UltraTextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::endpoints::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
|
||||
|
||||
+3
-122
@@ -1,130 +1,11 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, ImageModel};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use anyhow::anyhow;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
|
||||
use log::{error, info};
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_image_fal(
|
||||
app: &AppHandle,
|
||||
request: &EnqueueTextToImageRequest,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let api_key = match fal_creds_manager.get_key()? {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
error!("No FAL API key is set. Can't perform action.");
|
||||
let event =
|
||||
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::GenerateImage);
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(GenerateError::needs_fal_api_key());
|
||||
},
|
||||
};
|
||||
|
||||
let api_key = fal_creds_manager.get_key_required()
|
||||
.map_err(|err| {
|
||||
error!("EnqueueTextToImage FAL api key required: {:?}", err);
|
||||
GenerateError::needs_fal_api_key()
|
||||
})?;
|
||||
|
||||
let prompt = request.prompt.as_deref().unwrap_or("");
|
||||
|
||||
let model;
|
||||
|
||||
let result = match request.model {
|
||||
None => {
|
||||
return Err(GenerateError::BadInput(BadInputReason::NoModelSpecified));
|
||||
}
|
||||
Some(
|
||||
ImageModel::GptImage1 |
|
||||
ImageModel::Midjourney
|
||||
) => {
|
||||
return Err(GenerateError::AnyhowError(anyhow!("wrong logic: another branch should handle this: {:?}", request.model)));
|
||||
}
|
||||
Some(
|
||||
ImageModel::Gemini25Flash |
|
||||
ImageModel::Flux1Dev |
|
||||
ImageModel::Flux1Schnell |
|
||||
ImageModel::FluxPro11
|
||||
) => {
|
||||
return Err(GenerateError::NotYetImplemented(format!("not yet implemented: {:?}", request.model)));
|
||||
}
|
||||
Some(ImageModel::FluxPro11Ultra) => {
|
||||
model = GenerationModel::FluxPro11Ultra;
|
||||
info!("enqueue Flux Pro 1.1 Ultra text-to-image with prompt: {}", prompt);
|
||||
enqueue_flux_pro_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
|
||||
prompt,
|
||||
api_key: &api_key,
|
||||
}).await
|
||||
}
|
||||
Some(ImageModel::Recraft3) => {
|
||||
model = GenerationModel::Recraft3;
|
||||
info!("enqueue Recraft v3 text-to-image with prompt: {}", prompt);
|
||||
enqueue_recraft3_text_to_image(Recraft3TextToImageArgs {
|
||||
prompt,
|
||||
api_key: &api_key,
|
||||
}).await
|
||||
}
|
||||
};
|
||||
|
||||
let success_result = match result {
|
||||
Ok(enqueued) => {
|
||||
info!("Successfully enqueued text to image");
|
||||
|
||||
//let event = GenerationEnqueueSuccessEvent {
|
||||
// action: GenerationAction::GenerateImage,
|
||||
// service: GenerationServiceProvider::Fal,
|
||||
// model: None,
|
||||
//};
|
||||
|
||||
//if let Err(err) = event.send(app) {
|
||||
// error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
//}
|
||||
|
||||
if let Err(err) = fal_task_queue.insert(&enqueued) {
|
||||
error!("Failed to enqueue task: {:?}", err);
|
||||
return Err(GenerateError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
|
||||
}
|
||||
|
||||
enqueued
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to enqueue text to image: {:?}", err);
|
||||
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(GenerateError::from(err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Fal,
|
||||
task_type: TaskType::ImageGeneration,
|
||||
model: Some(model),
|
||||
provider_job_id: Some(success_result.request_id.to_string()),
|
||||
})
|
||||
Err(GenerateError::FalNoLongerSupported)
|
||||
}
|
||||
|
||||
-1
@@ -16,7 +16,6 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
|
||||
-1
@@ -10,7 +10,6 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
|
||||
-1
@@ -18,7 +18,6 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use midjourney_client::client::midjourney_hostname::MidjourneyHostname;
|
||||
|
||||
@@ -5,8 +5,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
@@ -5,8 +5,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -20,6 +18,8 @@ use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend;
|
||||
use storyteller_client::endpoints::media_files::delete_media_file::delete_media_file;
|
||||
use tauri::{AppHandle, State};
|
||||
use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
@@ -36,11 +36,33 @@ pub struct TaskQueueItem {
|
||||
pub model_type: Option<TaskModelType>,
|
||||
pub provider: Option<GenerationProvider>,
|
||||
pub provider_job_id: Option<String>,
|
||||
pub completed_item: Option<CompletedItemData>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CompletedItemData {
|
||||
pub primary_media_file: MediaFileData,
|
||||
|
||||
/// The type of file(s) generated.
|
||||
pub media_file_class: Option<TaskMediaFileClass>,
|
||||
|
||||
/// If generated in a batch, we probably have a batch token we can query.
|
||||
pub maybe_batch_token: Option<BatchGenerationToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MediaFileData {
|
||||
pub token: MediaFileToken,
|
||||
pub cdn_url: String,
|
||||
pub maybe_thumbnail_url_template: Option<String>,
|
||||
|
||||
// NB: The frontend wants this.
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl SerializeMarker for GetTaskQueueCommandResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -76,17 +98,45 @@ pub async fn handle_request(
|
||||
let tasks = list_tasks_for_frontend(task_database.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(tasks.tasks.into_iter()
|
||||
.map(|task| TaskQueueItem {
|
||||
id: task.id,
|
||||
task_status: task.status,
|
||||
task_type: task.task_type,
|
||||
model_type: task.model_type,
|
||||
provider: task.provider,
|
||||
provider_job_id: task.provider_job_id,
|
||||
created_at: task.created_at,
|
||||
updated_at: task.updated_at,
|
||||
completed_at: task.completed_at,
|
||||
})
|
||||
.collect())
|
||||
let mut transformed_tasks = Vec::with_capacity(tasks.tasks.len());
|
||||
|
||||
for task in tasks.tasks.into_iter() {
|
||||
let mut completed_item = None;
|
||||
|
||||
if task.status == TaskStatus::CompleteSuccess {
|
||||
let token_and_url = task.on_complete_primary_media_file_token
|
||||
.zip(task.on_complete_primary_media_file_cdn_url);
|
||||
|
||||
if let Some((primary_media_file_token, media_file_url)) = token_and_url{
|
||||
completed_item = Some(CompletedItemData {
|
||||
primary_media_file: MediaFileData {
|
||||
token: primary_media_file_token,
|
||||
cdn_url: media_file_url.clone(),
|
||||
maybe_thumbnail_url_template: task.on_complete_primary_media_file_thumbnail_url_template.clone(),
|
||||
// NB: This isn't the exact completion date. Also, fallback to now if missing.
|
||||
created_at: task.completed_at.unwrap_or_else(Utc::now),
|
||||
},
|
||||
media_file_class: task.on_complete_primary_media_file_class,
|
||||
maybe_batch_token: task.on_complete_batch_token,
|
||||
});
|
||||
} else {
|
||||
warn!("Task {} is marked complete but has no primary media file token or URL.", task.id);
|
||||
}
|
||||
}
|
||||
|
||||
transformed_tasks.push(TaskQueueItem {
|
||||
id: task.id,
|
||||
task_status: task.status,
|
||||
task_type: task.task_type,
|
||||
model_type: task.model_type,
|
||||
provider: task.provider,
|
||||
provider_job_id: task.provider_job_id,
|
||||
created_at: task.created_at,
|
||||
updated_at: task.updated_at,
|
||||
completed_at: task.completed_at,
|
||||
completed_item,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(transformed_tasks)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
@@ -3,15 +3,12 @@ use crate::core::lifecycle::startup::tasks::initially_size_and_position_windows:
|
||||
use crate::core::lifecycle::startup::tasks::load_provider_priority_state::load_provider_priority_state;
|
||||
use crate::core::lifecycle::startup::tasks::set_app_log_level::set_app_log_level;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_discord_presence_thread::spawn_discord_presence_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_fal_task_polling_thread::spawn_fal_task_polling_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_main_window_thread::spawn_main_window_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_sora_task_polling_thread::spawn_sora_task_polling_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_storyteller_threads::spawn_storyteller_threads;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use crate::services::midjourney::threads::midjourney_long_polling_thread::midjourney_long_polling_thread;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
@@ -28,8 +25,6 @@ pub async fn handle_tauri_startup(
|
||||
storyteller_creds_manager: StorytellerCredentialManager,
|
||||
sora_credential_manager: SoraCredentialManager,
|
||||
sora_task_queue: SoraTaskQueue,
|
||||
fal_credential_manager: FalCredentialManager,
|
||||
fal_task_queue: FalTaskQueue,
|
||||
mj_creds_manager: MidjourneyCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
@@ -70,15 +65,6 @@ pub async fn handle_tauri_startup(
|
||||
&sora_task_queue,
|
||||
)?;
|
||||
|
||||
spawn_fal_task_polling_thread(
|
||||
&app,
|
||||
&root,
|
||||
&app_env_configs,
|
||||
&fal_credential_manager,
|
||||
&fal_task_queue,
|
||||
&storyteller_creds_manager,
|
||||
)?;
|
||||
|
||||
tauri::async_runtime::spawn(midjourney_long_polling_thread(
|
||||
app.clone(),
|
||||
app_env_configs.clone(),
|
||||
|
||||
@@ -2,7 +2,6 @@ pub (super) mod bootstrap_task_database;
|
||||
pub (super) mod set_app_log_level;
|
||||
pub (super) mod initially_size_and_position_windows;
|
||||
pub (super) mod spawn_discord_presence_thread;
|
||||
pub (super) mod spawn_fal_task_polling_thread;
|
||||
pub (super) mod spawn_main_window_thread;
|
||||
pub (super) mod spawn_sora_task_polling_thread;
|
||||
pub (super) mod spawn_storyteller_threads;
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub fn spawn_fal_task_polling_thread(
|
||||
app: &AppHandle,
|
||||
root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_credential_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
tauri::async_runtime::spawn(fal_task_polling_thread(
|
||||
app.clone(),
|
||||
app_env_configs.clone(),
|
||||
root.clone(),
|
||||
fal_credential_manager.clone(),
|
||||
fal_task_queue.clone(),
|
||||
storyteller_creds_manager.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -7,4 +7,4 @@
|
||||
/// We increment this whenever we make a change to the database schema.
|
||||
/// This prevents deadlock on startup if the schema has changed from the
|
||||
/// previous version.
|
||||
pub const TASK_DATABASE_VERSION: u32 = 4;
|
||||
pub const TASK_DATABASE_VERSION: u32 = 5;
|
||||
|
||||
@@ -29,14 +29,6 @@ use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::threads::discord_presence_thread::discord_presence_thread;
|
||||
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
|
||||
use crate::services::fal::commands::fal_background_removal_command::fal_background_removal_command;
|
||||
use crate::services::fal::commands::fal_hunyuan_image_to_3d_command::fal_hunyuan_image_to_3d_command;
|
||||
use crate::services::fal::commands::fal_kling_image_to_video_command::fal_kling_image_to_video_command;
|
||||
use crate::services::fal::commands::get_fal_api_key_command::get_fal_api_key_command;
|
||||
use crate::services::fal::commands::set_fal_api_key_command::set_fal_api_key_command;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread;
|
||||
use crate::services::midjourney::commands::midjourney_clear_credentials_command::midjourney_clear_credentials_command;
|
||||
use crate::services::midjourney::commands::midjourney_get_credential_info_command::midjourney_get_credential_info_command;
|
||||
use crate::services::midjourney::commands::midjourney_open_login_command::midjourney_open_login_command;
|
||||
@@ -93,17 +85,10 @@ pub fn run() {
|
||||
let sora_creds_manager = SoraCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
let sora_creds_manager_2 = sora_creds_manager.clone();
|
||||
|
||||
println!("Attempting to read existing fal credentials...");
|
||||
let fal_creds_manager = FalCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
let fal_creds_manager_2 = fal_creds_manager.clone();
|
||||
|
||||
// Other state
|
||||
let sora_task_queue = SoraTaskQueue::new();
|
||||
let sora_task_queue_2 = sora_task_queue.clone();
|
||||
|
||||
let fal_task_queue = FalTaskQueue::new();
|
||||
let fal_task_queue_2 = fal_task_queue.clone();
|
||||
|
||||
let app_env_configs = AppEnvConfigs::load_from_filesystem(&app_data_root)
|
||||
.expect("AppEnvConfigs should be loaded from disk");
|
||||
|
||||
@@ -136,8 +121,6 @@ pub fn run() {
|
||||
let storyteller_creds = storyteller_creds_manager_2.clone();
|
||||
let sora_creds = sora_creds_manager_2.clone();
|
||||
let sora_tasks = sora_task_queue_2.clone();
|
||||
let fal_creds = fal_creds_manager_2.clone();
|
||||
let fal_tasks = fal_task_queue_2.clone();
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let result = setup_main_window(&app).await;
|
||||
@@ -150,8 +133,6 @@ pub fn run() {
|
||||
storyteller_creds,
|
||||
sora_creds,
|
||||
sora_tasks,
|
||||
fal_creds,
|
||||
fal_tasks,
|
||||
midjourney_creds_manager_2,
|
||||
).await;
|
||||
|
||||
@@ -167,8 +148,6 @@ pub fn run() {
|
||||
.manage(app_env_configs)
|
||||
.manage(app_preferences)
|
||||
.manage(artcraft_platform_info)
|
||||
.manage(fal_creds_manager)
|
||||
.manage(fal_task_queue)
|
||||
.manage(midjourney_creds_manager)
|
||||
.manage(sora_creds_manager)
|
||||
.manage(sora_task_queue)
|
||||
@@ -184,13 +163,9 @@ pub fn run() {
|
||||
enqueue_image_to_3d_object_command,
|
||||
enqueue_image_to_video_command,
|
||||
enqueue_text_to_image_command,
|
||||
fal_background_removal_command,
|
||||
fal_hunyuan_image_to_3d_command,
|
||||
fal_kling_image_to_video_command,
|
||||
flip_image,
|
||||
get_app_info_command,
|
||||
get_app_preferences_command,
|
||||
get_fal_api_key_command,
|
||||
get_provider_order_command,
|
||||
get_task_queue_command,
|
||||
load_without_cors_command,
|
||||
@@ -201,7 +176,6 @@ pub fn run() {
|
||||
midjourney_open_login_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
set_fal_api_key_command,
|
||||
set_provider_order_command,
|
||||
sora_get_credential_info_command,
|
||||
sora_logout_command,
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
use crate::core::artcraft_error::ArtcraftError;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::{DecodeError, Engine};
|
||||
use errors::AnyhowError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use fal_client::requests::queue::remove_background_rembg::remove_background_rembg;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use image::{EncodableLayout, ImageReader};
|
||||
use log::{error, info, warn};
|
||||
use reqwest::Url;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::endpoints::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
|
||||
use storyteller_client::endpoints::media_files::upload_image_media_file_from_file::{upload_image_media_file_from_file, UploadImageFromFileArgs};
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FalBackgroundRemovalRequest {
|
||||
/// Image media file; the image to remove the background from.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Base64-encoded image
|
||||
pub base64_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FalBackgroundRemovalSuccessResponse {
|
||||
/// Result media token
|
||||
pub media_token: MediaFileToken,
|
||||
/// Result URL
|
||||
pub cdn_url: Url,
|
||||
/// Base64 bytes
|
||||
pub base64_bytes: String,
|
||||
}
|
||||
|
||||
impl SerializeMarker for FalBackgroundRemovalSuccessResponse {}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BackgroundRemovalErrorType {
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
NeedsFalApiKey,
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fal_background_removal_command(
|
||||
app: AppHandle,
|
||||
request: FalBackgroundRemovalRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
) -> Response<FalBackgroundRemovalSuccessResponse, BackgroundRemovalErrorType, ()> {
|
||||
|
||||
info!("fal_background_removal_command called; image media token: {:?}", request.image_media_token);
|
||||
|
||||
let has_credentials = fal_creds_manager
|
||||
.has_apparent_api_token()
|
||||
.unwrap_or(true); // NB: Failures would be lock issues
|
||||
|
||||
if !has_credentials {
|
||||
warn!("No API key found");
|
||||
let event =
|
||||
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::RemoveBackground);
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::Unauthorized,
|
||||
error_message: Some("You need to set a FAL api key".to_string()),
|
||||
error_type: Some(BackgroundRemovalErrorType::NeedsFalApiKey),
|
||||
error_details: None,
|
||||
});
|
||||
}
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::RemoveBackground,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
let result = remove_background(
|
||||
request,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("error: {:?}", err);
|
||||
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::RemoveBackground,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = BackgroundRemovalErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
|
||||
|
||||
Err(CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
})
|
||||
}
|
||||
Ok(result) => {
|
||||
Ok(FalBackgroundRemovalSuccessResponse {
|
||||
media_token: result.0.media_file.token,
|
||||
cdn_url: result.0.media_file.media_links.cdn_url,
|
||||
base64_bytes: result.1,
|
||||
}.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InnerError {
|
||||
FalError(FalErrorPlus),
|
||||
AnyhowError(AnyhowError),
|
||||
StorytellerError(StorytellerError),
|
||||
DecodeError(DecodeError),
|
||||
IoError(std::io::Error),
|
||||
ArtcraftError(ArtcraftError),
|
||||
}
|
||||
|
||||
impl From<AnyhowError> for InnerError {
|
||||
fn from(value: AnyhowError) -> Self {
|
||||
Self::AnyhowError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FalErrorPlus> for InnerError {
|
||||
fn from(value: FalErrorPlus) -> Self {
|
||||
Self::FalError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StorytellerError> for InnerError {
|
||||
fn from(value: StorytellerError) -> Self {
|
||||
Self::StorytellerError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for InnerError {
|
||||
fn from(value: DecodeError) -> Self {
|
||||
Self::DecodeError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for InnerError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ArtcraftError> for InnerError {
|
||||
fn from(value: ArtcraftError) -> Self {
|
||||
match value {
|
||||
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
|
||||
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
|
||||
ArtcraftError::IoError(e) => Self::IoError(e),
|
||||
_ => Self::ArtcraftError(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_background(
|
||||
request: FalBackgroundRemovalRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<(GetMediaFileSuccessResponse, String), InnerError> {
|
||||
|
||||
let api_key = fal_creds_manager.get_key_required()?;
|
||||
let creds = storyteller_creds_manager.get_credentials_required()?;
|
||||
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
info!("From media token: {:?}", &media_token);
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else if let Some(base64_bytes) = request.base64_image {
|
||||
info!("From base64 bytes...");
|
||||
temp_download = save_base64_image_to_temp_dir(&app_data_root, &base64_bytes).await?;
|
||||
} else {
|
||||
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
|
||||
}
|
||||
|
||||
info!("Calling FAL image background removal...");
|
||||
|
||||
let filename = temp_download.path().to_path_buf();
|
||||
|
||||
let result = remove_background_rembg(filename, &api_key).await?;
|
||||
|
||||
info!("Unpacking FAL result...");
|
||||
|
||||
let extension_with_dot = get_url_file_extension(&result.image_url)
|
||||
.map(|ext| format!(".{}", ext))
|
||||
.unwrap_or_else(|| ".png".to_string());
|
||||
|
||||
let mut result_file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension_with_dot)?;
|
||||
|
||||
info!("Downloading file result file...");
|
||||
|
||||
simple_http_download_to_tempfile(&result.image_url, &mut result_file).await?;
|
||||
|
||||
let result_filename = result_file.path().to_path_buf();
|
||||
|
||||
// NB: We couldn't pass the existing file handle as I think the file handle pointer is already at the EOF
|
||||
let bytes = file_read_bytes(&result_filename)?;
|
||||
let base64_bytes = BASE64_STANDARD.encode(bytes.as_bytes());
|
||||
|
||||
info!("Uploading image media file...");
|
||||
|
||||
let upload_result = upload_image_media_file_from_file(UploadImageFromFileArgs {
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(&creds),
|
||||
path: result_filename,
|
||||
is_intermediate_system_file: true, // NB: Probably not essential to keep this.
|
||||
maybe_prompt_token: None, // NB: Not used for bg removal.
|
||||
maybe_batch_token: None, // NB: Not used for bg removal.
|
||||
}).await?;
|
||||
|
||||
// TODO: Don't re-request to simply build MediaLinks (or CDN URL). Get those from the upload API in one turn.
|
||||
info!("Re-requesting media file...");
|
||||
|
||||
let response = get_media_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
&upload_result.media_file_token,
|
||||
).await?;
|
||||
|
||||
info!("Uploaded media file: {:?}", response.media_file.token);
|
||||
|
||||
Ok((response, base64_bytes))
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
use crate::core::artcraft_error::ArtcraftError;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use base64::DecodeError;
|
||||
use errors::AnyhowError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FalHunyuanImageTo3dRequest {
|
||||
/// Image media file; the image to remove the background from.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Base64-encoded image
|
||||
pub base64_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HunyuanErrorType {
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
NeedsFalApiKey,
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fal_hunyuan_image_to_3d_command(
|
||||
app: AppHandle,
|
||||
request: FalHunyuanImageTo3dRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
) -> Response<(), HunyuanErrorType, ()> {
|
||||
|
||||
info!("fal_hunyuan_image_to_3d_command called; image media token: {:?}", request.image_media_token);
|
||||
|
||||
let has_credentials = fal_creds_manager
|
||||
.has_apparent_api_token()
|
||||
.unwrap_or(true); // NB: Failures would be lock issues
|
||||
|
||||
if !has_credentials {
|
||||
warn!("No API key found");
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::Unauthorized,
|
||||
error_message: Some("You need to set a FAL api key".to_string()),
|
||||
error_type: Some(HunyuanErrorType::NeedsFalApiKey),
|
||||
error_details: None,
|
||||
});
|
||||
}
|
||||
|
||||
let result = image_to_3d(
|
||||
request,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("error: {:?}", err);
|
||||
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::ImageTo3d,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = HunyuanErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
|
||||
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
})
|
||||
}
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::ImageTo3d,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InnerError {
|
||||
FalError(FalErrorPlus),
|
||||
AnyhowError(AnyhowError),
|
||||
StorytellerError(StorytellerError),
|
||||
DecodeError(DecodeError),
|
||||
IoError(std::io::Error),
|
||||
ArtcraftError(ArtcraftError),
|
||||
}
|
||||
|
||||
impl From<AnyhowError> for InnerError {
|
||||
fn from(value: AnyhowError) -> Self {
|
||||
Self::AnyhowError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FalErrorPlus> for InnerError {
|
||||
fn from(value: FalErrorPlus) -> Self {
|
||||
Self::FalError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StorytellerError> for InnerError {
|
||||
fn from(value: StorytellerError) -> Self {
|
||||
Self::StorytellerError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for InnerError {
|
||||
fn from(value: DecodeError) -> Self {
|
||||
Self::DecodeError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for InnerError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ArtcraftError> for InnerError {
|
||||
fn from(value: ArtcraftError) -> Self {
|
||||
match value {
|
||||
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
|
||||
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
|
||||
ArtcraftError::IoError(e) => Self::IoError(e),
|
||||
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
|
||||
_ => Self::ArtcraftError(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn image_to_3d(
|
||||
request: FalHunyuanImageTo3dRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InnerError> {
|
||||
|
||||
let api_key = fal_creds_manager.get_key_required()?;
|
||||
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
} else if let Some(base64_bytes) = request.base64_image {
|
||||
temp_download = save_base64_image_to_temp_dir(&app_data_root, &base64_bytes).await?;
|
||||
} else {
|
||||
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
|
||||
}
|
||||
|
||||
info!("Calling FAL image to 3d ...");
|
||||
|
||||
let filename = temp_download.path().to_path_buf();
|
||||
|
||||
let enqueued = enqueue_hunyuan2_image_to_3d(Hunyuan2Args {
|
||||
image_path: filename,
|
||||
api_key: &api_key,
|
||||
}).await?;
|
||||
|
||||
if let Err(err) = fal_task_queue.insert(&enqueued) {
|
||||
error!("Failed to enqueue task: {:?}", err);
|
||||
return Err(InnerError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
-224
@@ -1,224 +0,0 @@
|
||||
use crate::core::artcraft_error::ArtcraftError;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use base64::DecodeError;
|
||||
use errors::AnyhowError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FalKlingImageToVideoRequest {
|
||||
/// Image media file; the image to remove the background from.
|
||||
/// This must be supplied **OR** the `base64_image`.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Base64-encoded image
|
||||
/// This must be supplied **OR** the `image_media_token`.
|
||||
pub base64_image: Option<String>,
|
||||
|
||||
/// Optional: Text prompt.
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum KlingImageToVideoErrorType {
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
NeedsFalApiKey,
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fal_kling_image_to_video_command(
|
||||
app: AppHandle,
|
||||
request: FalKlingImageToVideoRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
) -> Response<(), KlingImageToVideoErrorType, ()> {
|
||||
|
||||
info!("fal_kling_image_to_video_command called; image media token: {:?}", request.image_media_token);
|
||||
|
||||
let has_credentials = fal_creds_manager
|
||||
.has_apparent_api_token()
|
||||
.unwrap_or(true); // NB: Failures would be lock issues
|
||||
|
||||
if !has_credentials {
|
||||
warn!("No API key found");
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::Unauthorized,
|
||||
error_message: Some("You need to set a FAL api key".to_string()),
|
||||
error_type: Some(KlingImageToVideoErrorType::NeedsFalApiKey),
|
||||
error_details: None,
|
||||
});
|
||||
}
|
||||
|
||||
let result = image_to_video(
|
||||
request,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("error: {:?}", err);
|
||||
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::GenerateVideo,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = KlingImageToVideoErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
|
||||
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
})
|
||||
}
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateVideo,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InnerError {
|
||||
FalError(FalErrorPlus),
|
||||
AnyhowError(AnyhowError),
|
||||
StorytellerError(StorytellerError),
|
||||
DecodeError(DecodeError),
|
||||
IoError(std::io::Error),
|
||||
ArtcraftError(ArtcraftError),
|
||||
}
|
||||
|
||||
impl From<AnyhowError> for InnerError {
|
||||
fn from(value: AnyhowError) -> Self {
|
||||
Self::AnyhowError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FalErrorPlus> for InnerError {
|
||||
fn from(value: FalErrorPlus) -> Self {
|
||||
Self::FalError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StorytellerError> for InnerError {
|
||||
fn from(value: StorytellerError) -> Self {
|
||||
Self::StorytellerError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for InnerError {
|
||||
fn from(value: DecodeError) -> Self {
|
||||
Self::DecodeError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for InnerError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ArtcraftError> for InnerError {
|
||||
fn from(value: ArtcraftError) -> Self {
|
||||
match value {
|
||||
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
|
||||
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
|
||||
ArtcraftError::IoError(e) => Self::IoError(e),
|
||||
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
|
||||
_ => Self::ArtcraftError(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn image_to_video(
|
||||
request: FalKlingImageToVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InnerError> {
|
||||
|
||||
let api_key = fal_creds_manager.get_key_required()?;
|
||||
let creds = storyteller_creds_manager.get_credentials_required()?;
|
||||
|
||||
let mut temp_download;
|
||||
|
||||
if let Some(media_token) = request.image_media_token {
|
||||
temp_download = download_media_file_to_temp_dir(
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
&media_token
|
||||
).await?;
|
||||
|
||||
} else if let Some(base64_bytes) = request.base64_image {
|
||||
temp_download = save_base64_image_to_temp_dir(&app_data_root, &base64_bytes).await?;
|
||||
} else {
|
||||
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
|
||||
}
|
||||
|
||||
info!("Calling FAL image to video ...");
|
||||
|
||||
let filename = temp_download.path().to_path_buf();
|
||||
|
||||
let enqueued = enqueue_kling_16_pro_image_to_video(Kling16ProArgs {
|
||||
image_path: filename,
|
||||
api_key: &api_key,
|
||||
duration: Kling16ProDuration::Default,
|
||||
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
|
||||
prompt: request.prompt.as_deref().unwrap_or(""),
|
||||
}).await?;
|
||||
|
||||
if let Err(err) = fal_task_queue.insert(&enqueued) {
|
||||
error!("Failed to enqueue task: {:?}", err);
|
||||
return Err(InnerError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use log::error;
|
||||
use serde_derive::Serialize;
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetFalApiKeyResponse {
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
impl SerializeMarker for GetFalApiKeyResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_fal_api_key_command(
|
||||
creds_manager: State<'_, FalCredentialManager>,
|
||||
) -> Response<GetFalApiKeyResponse, (), ()> {
|
||||
|
||||
let key = get_key(&creds_manager)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error getting API key: {:?}", err);
|
||||
"error getting key"
|
||||
})?;
|
||||
|
||||
Ok(key.into())
|
||||
}
|
||||
|
||||
async fn get_key(
|
||||
creds_manager: &FalCredentialManager,
|
||||
) -> AnyhowResult<GetFalApiKeyResponse> {
|
||||
let maybe_key = creds_manager.get_key()?
|
||||
.map(|key| key.0)
|
||||
.map(|key| key.trim().to_string())
|
||||
.filter(|key| !key.is_empty());
|
||||
|
||||
Ok(GetFalApiKeyResponse { key: maybe_key })
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod fal_background_removal_command;
|
||||
pub mod fal_hunyuan_image_to_3d_command;
|
||||
pub mod fal_kling_image_to_video_command;
|
||||
pub mod get_fal_api_key_command;
|
||||
pub mod set_fal_api_key_command;
|
||||
// pub mod get_open_ai_api_key_command;
|
||||
// pub mod set_open_ai_api_key_command;
|
||||
@@ -1,54 +0,0 @@
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use log::{error, info};
|
||||
use serde_derive::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
/// For now, we'll only update a single preference at a time.
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetFalApiKeyRequest{
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_fal_api_key_command(
|
||||
request: SetFalApiKeyRequest,
|
||||
creds_manager: State<'_, FalCredentialManager>,
|
||||
) -> Response<(), (), ()> {
|
||||
info!("update_app_preferences_command called");
|
||||
|
||||
set_key(request, &creds_manager)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error getting app preferences: {:?}", err);
|
||||
"error setting key"
|
||||
})?;
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
async fn set_key(
|
||||
request: SetFalApiKeyRequest,
|
||||
creds_manager: &FalCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let key = request.key
|
||||
.map(|key| key.trim().to_string())
|
||||
.filter(|key| !key.is_empty())
|
||||
.map(|key| FalApiKey::from_str(&key));
|
||||
|
||||
match key {
|
||||
None => {
|
||||
creds_manager.clear_key()?;
|
||||
creds_manager.purge_api_key_from_disk()?;
|
||||
}
|
||||
Some(key) => {
|
||||
creds_manager.set_key(&key)?;
|
||||
creds_manager.persist_to_disk()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod commands;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
@@ -1,100 +0,0 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use memory_store::clone_slot::CloneSlot;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::{fs, io};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FalCredentialManager {
|
||||
key: CloneSlot<FalApiKey>,
|
||||
app_data_root: AppDataRoot,
|
||||
}
|
||||
|
||||
impl FalCredentialManager {
|
||||
pub fn new(app_data_root: &AppDataRoot) -> Self {
|
||||
Self {
|
||||
key: CloneSlot::empty(),
|
||||
app_data_root: app_data_root.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
|
||||
let mut holder = CloneSlot::empty();
|
||||
|
||||
match read_fal_api_key_from_disk(app_data_root) {
|
||||
Err(err) => {
|
||||
log::warn!("Failed to read FAL API key from disk: {:?}", err);
|
||||
}
|
||||
Ok(api_key) => {
|
||||
holder.set_clone(&api_key).expect("Failed to set FAL API key");
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
key: holder,
|
||||
app_data_root: app_data_root.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_key(&self, key: &FalApiKey) -> AnyhowResult<()> {
|
||||
if key.0.trim().is_empty() {
|
||||
return Err(anyhow!("FAL API key cannot be empty")); // TODO: Also handle upstream.
|
||||
}
|
||||
self.key.set_clone(key)
|
||||
}
|
||||
|
||||
pub fn get_key(&self) -> AnyhowResult<Option<FalApiKey>> {
|
||||
self.key.get_clone()
|
||||
}
|
||||
|
||||
pub fn get_key_required(&self) -> AnyhowResult<FalApiKey> {
|
||||
self.key.get_clone_required()
|
||||
}
|
||||
|
||||
pub fn clear_key(&self) -> AnyhowResult<()> {
|
||||
self.key.clear()
|
||||
}
|
||||
|
||||
/// Does not check the token for validity
|
||||
pub fn has_apparent_api_token(&self) -> AnyhowResult<bool> {
|
||||
self.key.is_some()
|
||||
}
|
||||
|
||||
pub fn purge_api_key_from_disk(&self) -> Result<(), io::Error> {
|
||||
let filename = self.app_data_root.credentials_dir().get_fal_api_key_file_path();
|
||||
if filename.exists() && filename.is_file() {
|
||||
fs::remove_file(filename)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn persist_to_disk(&self) -> AnyhowResult<()> {
|
||||
let key = match self.get_key() {
|
||||
Ok(Some(key)) => key,
|
||||
Ok(None) => {
|
||||
self.purge_api_key_from_disk()?;
|
||||
return Ok(());
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(anyhow!("Failed to get FAL key: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
let filename = self.app_data_root.credentials_dir().get_fal_api_key_file_path();
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filename)?;
|
||||
|
||||
file.write_all(key.0.as_bytes())?;
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use crate::core::events::generation_events::common::GenerationAction;
|
||||
use fal_client::model::fal_endpoint::FalEndpoint;
|
||||
|
||||
/// Adapter of generation types/categories for FAL
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FalGenerationType {
|
||||
TextToImage,
|
||||
ImageToImage,
|
||||
TextToVideo,
|
||||
ImageToVideo,
|
||||
ImageTo3d,
|
||||
ImageBackgroundRemoval,
|
||||
}
|
||||
|
||||
impl FalGenerationType {
|
||||
pub fn from_fal_endpoint(endpoint: &FalEndpoint) -> Option<Self> {
|
||||
match endpoint {
|
||||
FalEndpoint::FluxProUltraTextToImage => Some(Self::TextToImage),
|
||||
FalEndpoint::Hunyuan3d2Base => Some(Self::ImageTo3d),
|
||||
FalEndpoint::Kling16ImageToVideo => Some(Self::ImageToVideo),
|
||||
FalEndpoint::Minimax01ImageToVideo => Some(Self::ImageToVideo),
|
||||
FalEndpoint::RecraftV3TextToImage => Some(Self::TextToImage),
|
||||
FalEndpoint::Other(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_event_generation_action(&self) -> GenerationAction {
|
||||
match self {
|
||||
Self::TextToImage => GenerationAction::GenerateImage,
|
||||
Self::ImageToImage => GenerationAction::GenerateImage,
|
||||
Self::TextToVideo => GenerationAction::GenerateVideo,
|
||||
Self::ImageToVideo => GenerationAction::GenerateVideo,
|
||||
Self::ImageTo3d => GenerationAction::ImageTo3d,
|
||||
Self::ImageBackgroundRemoval => GenerationAction::RemoveBackground,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use crate::services::fal::state::fal_generation_type::FalGenerationType;
|
||||
use chrono::{DateTime, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::model::enqueued_request::EnqueuedRequest;
|
||||
use fal_client::model::fal_request_id::FalRequestId;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FalTaskDetails {
|
||||
pub enqueue_time: DateTime<Utc>,
|
||||
pub enqueued_request: EnqueuedRequest,
|
||||
pub generation_type: Option<FalGenerationType>,
|
||||
}
|
||||
|
||||
impl FalTaskDetails {
|
||||
pub fn for_request(request: EnqueuedRequest) -> Self {
|
||||
Self {
|
||||
enqueue_time: Utc::now(),
|
||||
generation_type: FalGenerationType::from_fal_endpoint(&request.fal_endpoint),
|
||||
enqueued_request: request,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FalTaskQueue {
|
||||
// Insertion-order map of task IDs to task details
|
||||
queue: Arc<RwLock<IndexMap<FalRequestId, FalTaskDetails>>>
|
||||
}
|
||||
|
||||
impl FalTaskQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: Arc::new(RwLock::new(IndexMap::new()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if queue is empty
|
||||
/// This is an O(1) operation.
|
||||
pub fn is_empty(&self) -> AnyhowResult<bool> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => Ok(lock.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get length of set
|
||||
/// This is an O(1) operation.
|
||||
pub fn len(&self) -> AnyhowResult<usize> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => Ok(lock.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the task at an index.
|
||||
/// This is an O(1) operation.
|
||||
pub fn get_index(&self, i: usize) -> AnyhowResult<Option<FalTaskDetails>> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => {
|
||||
if i >= lock.len() {
|
||||
Ok(None)
|
||||
} else {
|
||||
match lock.get_index(i) {
|
||||
None => Ok(None),
|
||||
Some((_key, value)) => {
|
||||
Ok(Some(value.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if the key is in the queue
|
||||
/// This is an O(1) operation.
|
||||
pub fn contains_key(&self, request_id: &FalRequestId) -> AnyhowResult<bool> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => Ok(lock.contains_key(request_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the first enqueued task
|
||||
/// This is an O(1) operation.
|
||||
pub fn first(&self) -> AnyhowResult<Option<FalTaskDetails>> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => match lock.first() {
|
||||
None => Ok(None),
|
||||
Some((request_id, task_details)) => Ok(Some(task_details.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last enqueued task
|
||||
/// This is an O(1) operation.
|
||||
pub fn last(&self) -> AnyhowResult<Option<FalTaskDetails>> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => match lock.last() {
|
||||
None => Ok(None),
|
||||
Some((request_id, task_details)) => Ok(Some(task_details.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a new task. If a task already exists, we update its value.
|
||||
/// This is an O(1) operation.
|
||||
pub fn insert(&self, request: &EnqueuedRequest) -> AnyhowResult<Option<FalTaskDetails>> {
|
||||
match self.queue.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
|
||||
}
|
||||
Ok(mut lock) => {
|
||||
// NB: Probably fine to compute TaskDetails here for now. We may eventually expose this if we include additional info.
|
||||
let task_details = FalTaskDetails::for_request(request.clone());
|
||||
Ok(lock.insert(request.request_id.clone(), task_details))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a task by ID.
|
||||
/// This is an O(n) operation.
|
||||
pub fn remove(&self, request_id: &FalRequestId) -> AnyhowResult<Option<FalTaskDetails>> {
|
||||
match self.queue.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
|
||||
}
|
||||
Ok(mut lock) => {
|
||||
Ok(lock.shift_remove(request_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a list of tasks by ID.
|
||||
/// This is an O(n) operation.
|
||||
pub fn remove_list(&self, request_ids: &[&FalRequestId]) -> AnyhowResult<()> {
|
||||
match self.queue.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
|
||||
}
|
||||
Ok(mut lock) => {
|
||||
// TODO: Cleaner way to do this?
|
||||
let mut set : HashSet<&FalRequestId, _> = HashSet::with_capacity(request_ids.len());
|
||||
set.extend(request_ids);
|
||||
lock.retain(|k, v| !set.contains(k));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod fal_credential_manager;
|
||||
pub mod fal_task_queue;
|
||||
pub mod read_fal_api_key_from_disk;
|
||||
mod fal_generation_type;
|
||||
@@ -1,19 +0,0 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
pub fn read_fal_api_key_from_disk(app_data_root: &AppDataRoot) -> AnyhowResult<FalApiKey> {
|
||||
let api_key_file = app_data_root.credentials_dir().get_fal_api_key_file_path();
|
||||
|
||||
if !api_key_file.exists() {
|
||||
return Err(anyhow!("API key file does not exist: {:?}", api_key_file));
|
||||
}
|
||||
|
||||
let value = read_to_string(api_key_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(FalApiKey::new(value))
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::GenerationServiceProvider;
|
||||
use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::download_url_to_temp_dir::download_url_to_temp_dir;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::export::queue::Status;
|
||||
use fal_client::model::fal_request_id::FalRequestId;
|
||||
use fal_client::utils::queue_status_checker::QueueStatusChecker;
|
||||
use log::{error, info, warn};
|
||||
use storyteller_client::recipes::upload_media_file_from_file::upload_media_file_from_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub async fn fal_task_polling_thread(
|
||||
app_handle: AppHandle,
|
||||
app_env_configs: AppEnvConfigs,
|
||||
app_data_root: AppDataRoot,
|
||||
fal_creds_manager: FalCredentialManager,
|
||||
fal_task_queue: FalTaskQueue,
|
||||
storyteller_creds_manager: StorytellerCredentialManager,
|
||||
) -> ! {
|
||||
loop {
|
||||
let res = polling_loop(
|
||||
&app_handle,
|
||||
&app_env_configs,
|
||||
&fal_creds_manager,
|
||||
&fal_task_queue,
|
||||
&storyteller_creds_manager,
|
||||
&app_data_root,
|
||||
).await;
|
||||
if let Err(err) = res {
|
||||
error!("An error occurred: {:?}", err);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn polling_loop(
|
||||
app_handle: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_data_root: &AppDataRoot,
|
||||
) -> AnyhowResult<()> {
|
||||
loop {
|
||||
if fal_task_queue.is_empty()? {
|
||||
// No need to poll if we don't have pending tasks.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Task queue has {} pending tasks.", fal_task_queue.len()?);
|
||||
|
||||
let key = fal_creds_manager.get_key_required()?;
|
||||
let storyteller_creds = storyteller_creds_manager.get_credentials_required()?;
|
||||
|
||||
let queue_status_checker = QueueStatusChecker::new(key);
|
||||
|
||||
let mut succeeded_tasks = Vec::new();
|
||||
//let mut failed_tasks = Vec::new(); // TODO
|
||||
|
||||
let len = fal_task_queue.len()?;
|
||||
|
||||
for i in 0..len {
|
||||
let task = match fal_task_queue.get_index(i)? {
|
||||
Some(task) => task,
|
||||
None => {
|
||||
warn!("Failed to get task ID at index {}", i);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let task_id = task.enqueued_request.request_id.clone();
|
||||
|
||||
info!("Checking task status for task ID: {:?}", &task_id);
|
||||
|
||||
let result = queue_status_checker.check_status(&task.enqueued_request).await;
|
||||
|
||||
let url = match result {
|
||||
Err(err) => {
|
||||
warn!("Error checking job status: {:?}", err);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
|
||||
|
||||
// TODO: Some failures will be permanent job failures and the jobs will need to be removed.
|
||||
|
||||
continue;
|
||||
}
|
||||
Ok(status) => {
|
||||
info!("Status: {:?}", status);
|
||||
match status.status {
|
||||
Status::Completed => {
|
||||
let result = queue_status_checker.get_download_url(&task.enqueued_request).await;
|
||||
|
||||
let url = match result {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
warn!("Failed to get download url: {:?}", err);
|
||||
|
||||
// TODO: Some failures will be permanent job failures.
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
url
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Clean up
|
||||
|
||||
let download_file = download_url_to_temp_dir(&url, &app_data_root).await?;
|
||||
|
||||
let result = upload_media_file_from_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&storyteller_creds),
|
||||
download_file,
|
||||
None, // TODO: Should probably save prompts.
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(success) => {
|
||||
info!("Uploaded to API backend: {:?}", success.media_file_token);
|
||||
|
||||
let event = GenerationCompleteEvent {
|
||||
//media_file_token: result.media_file_token,
|
||||
action: task.generation_type
|
||||
.map(|typ| typ.to_event_generation_action()),
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app_handle) {
|
||||
error!("Failed to send GenerationCompleteEvent: {:?}", err); // Fail open
|
||||
}
|
||||
|
||||
succeeded_tasks.push(task_id);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to upload media file: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// // TODO: The cursoring logic likely needs to improve.
|
||||
// for task in response.task_responses {
|
||||
// let status = TaskStatus::from_str(&task.status);
|
||||
//
|
||||
// match status {
|
||||
// TaskStatus::Succeeded => {
|
||||
// succeeded_tasks.push(task);
|
||||
// }
|
||||
// TaskStatus::Failed => {
|
||||
// failed_tasks.push(task);
|
||||
// }
|
||||
// TaskStatus::Queued => {}
|
||||
// TaskStatus::Running => {}
|
||||
// TaskStatus::Unknown(_) => {}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for task in failed_tasks.iter() {
|
||||
// if fal_task_queue.contains_key(&task.id)? {
|
||||
// let event = SoraImageGenerationFailedEvent {
|
||||
// prompt: task.prompt.clone(),
|
||||
// };
|
||||
// event.send(&app_handle)?;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let failed_task_ids: Vec<&TaskId> = failed_tasks
|
||||
// .iter()
|
||||
// .map(|task| &task.id)
|
||||
// .collect();
|
||||
//
|
||||
// fal_task_queue.remove_list(&failed_task_ids)?;
|
||||
//
|
||||
// let creds = storyteller_creds_manager.get_credentials_required()?;
|
||||
// let api_host = ApiHost::Storyteller;
|
||||
//
|
||||
// for task in succeeded_tasks.iter() {
|
||||
// if !fal_task_queue.contains_key(&task.id)? {
|
||||
// continue;
|
||||
// }
|
||||
// info!("Task succeeded: {:?}", task.id);
|
||||
//
|
||||
// for (i, generation) in task.generations.iter().enumerate() {
|
||||
// info!("Downloading generated file...");
|
||||
// let download_path = download_generation(generation, &app_data_root).await?;
|
||||
//
|
||||
// info!("Uploading to backend...");
|
||||
// let result = upload_image_media_file_from_file(&api_host, Some(&creds), download_path).await?;
|
||||
//
|
||||
// info!("Uploaded to API backend: {:?}", result.media_file_token);
|
||||
//
|
||||
// let event = SoraImageGenerationCompleteEvent {
|
||||
// media_file_token: result.media_file_token,
|
||||
// };
|
||||
//
|
||||
// event.send(&app_handle)?;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
let succeeded_task_ids : Vec<&FalRequestId> = succeeded_tasks
|
||||
.iter()
|
||||
.map(|task| task)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
fal_task_queue.remove_list(&succeeded_task_ids)?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod fal_task_polling_thread;
|
||||
+1
-3
@@ -1,12 +1,10 @@
|
||||
use crate::core::commands::response::shorthand::{Response, SimpleResponse};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use log::{error, info};
|
||||
use serde_derive::Deserialize;
|
||||
use tauri::State;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn midjourney_clear_credentials_command(
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorMessage};
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod fal;
|
||||
pub mod midjourney;
|
||||
pub mod sora;
|
||||
pub mod storyteller;
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorMessage};
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
+78
-4
@@ -17,26 +17,30 @@ use crate::services::storyteller::threads::events::maybe_handle_text_to_image_co
|
||||
use crate::services::storyteller::threads::events::maybe_send_background_removal_complete_event::maybe_send_background_removal_complete_event;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::jobs::list_session_jobs::ListSessionJobsItem;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use reqwest::Url;
|
||||
use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task};
|
||||
use sqlite_tasks::queries::update_successful_task_status_with_metadata::{update_successful_task_status_with_metadata, UpdateSuccessfulTaskArgs};
|
||||
use sqlite_tasks::queries::update_task_status::{update_task_status, UpdateTaskArgs};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use storyteller_client::error::api_error::ApiError;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::endpoints::jobs::list_session_jobs::{list_session_jobs, States};
|
||||
use storyteller_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use storyteller_client::error::api_error::ApiError;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use tauri::AppHandle;
|
||||
use tempdir::TempDir;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub async fn storyteller_task_polling_thread(
|
||||
app_handle: AppHandle,
|
||||
@@ -134,10 +138,23 @@ async fn polling_loop(
|
||||
_ => {} // Fall-through.
|
||||
}
|
||||
|
||||
let updated = update_task_status(UpdateTaskArgs {
|
||||
let maybe_primary_media_file_token = job.maybe_result
|
||||
.as_ref()
|
||||
.map(|result| MediaFileToken::new_from_str(&result.entity_token));
|
||||
|
||||
let updated = update_successful_task_status_with_metadata(UpdateSuccessfulTaskArgs {
|
||||
db: task_database.get_connection(),
|
||||
task_id: &task.id,
|
||||
status: TaskStatus::CompleteSuccess,
|
||||
maybe_batch_token: job.maybe_result
|
||||
.as_ref()
|
||||
.map(|result| result.maybe_batch_token.as_ref())
|
||||
.flatten(),
|
||||
maybe_primary_media_file_token: maybe_primary_media_file_token.as_ref(),
|
||||
maybe_primary_media_file_class: get_media_file_class(&job),
|
||||
maybe_primary_media_file_thumbnail_url_template: get_thumbnail_template(&job),
|
||||
maybe_primary_media_file_cdn_url: job.maybe_result
|
||||
.as_ref()
|
||||
.map(|result| result.media_links.cdn_url.as_str()),
|
||||
}).await?;
|
||||
|
||||
if !updated {
|
||||
@@ -212,3 +229,60 @@ async fn send_additional_events(
|
||||
error!("Failed to send background removal complete event: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_thumbnail_template<'a>(job: &'a ListSessionJobsItem) -> Option<&'a str> {
|
||||
let links = match job.maybe_result.as_ref() {
|
||||
None => return None,
|
||||
Some(result) => &result.media_links,
|
||||
};
|
||||
|
||||
// NB: We only populate video previews for video tasks.
|
||||
if let Some(video) = links.maybe_video_previews.as_ref() {
|
||||
return Some(&video.animated_thumbnail_template);
|
||||
}
|
||||
|
||||
// Last option - use the image thumbnail template if it exists.
|
||||
links.maybe_thumbnail_template.as_deref()
|
||||
}
|
||||
|
||||
fn get_media_file_class<'a>(job: &'a ListSessionJobsItem) -> Option<TaskMediaFileClass> {
|
||||
match job.request.inference_category {
|
||||
InferenceCategory::BackgroundRemoval => return Some(TaskMediaFileClass::Image),
|
||||
InferenceCategory::ImageGeneration => return Some(TaskMediaFileClass::Image),
|
||||
InferenceCategory::VideoGeneration => return Some(TaskMediaFileClass::Video),
|
||||
InferenceCategory::ObjectGeneration => return Some(TaskMediaFileClass::Dimensional),
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
let result = match job.maybe_result.as_ref() {
|
||||
None => return None,
|
||||
Some(result) => result,
|
||||
};
|
||||
|
||||
let url = result.media_links.cdn_url.as_str();
|
||||
|
||||
if url.ends_with("jpg")
|
||||
|| url.ends_with("jpeg")
|
||||
|| url.ends_with("png")
|
||||
{
|
||||
return Some(TaskMediaFileClass::Image);
|
||||
}
|
||||
|
||||
if url.ends_with("mp4")
|
||||
|| url.ends_with("webm")
|
||||
{
|
||||
return Some(TaskMediaFileClass::Video);
|
||||
}
|
||||
|
||||
if url.ends_with("glb") {
|
||||
return Some(TaskMediaFileClass::Dimensional);
|
||||
}
|
||||
|
||||
if url.ends_with("wav")
|
||||
|| url.ends_with("mp3")
|
||||
{
|
||||
return Some(TaskMediaFileClass::Audio);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ use crate::connection::TaskDbConnection;
|
||||
use crate::error::SqliteTasksError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
pub struct TaskList {
|
||||
@@ -22,6 +25,11 @@ pub struct TaskItem {
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
pub on_complete_primary_media_file_token: Option<MediaFileToken>,
|
||||
pub on_complete_primary_media_file_class: Option<TaskMediaFileClass>,
|
||||
pub on_complete_batch_token: Option<BatchGenerationToken>,
|
||||
pub on_complete_primary_media_file_cdn_url: Option<String>,
|
||||
pub on_complete_primary_media_file_thumbnail_url_template: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
@@ -43,6 +51,11 @@ pub async fn list_tasks_for_frontend(
|
||||
frontend_caller,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload,
|
||||
on_complete_primary_media_file_token,
|
||||
on_complete_primary_media_file_class,
|
||||
on_complete_batch_token,
|
||||
on_complete_primary_media_file_cdn_url,
|
||||
on_complete_primary_media_file_thumbnail_url_template,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
updated_at as "updated_at: DateTime<Utc>",
|
||||
completed_at as "completed_at: DateTime<Utc>"
|
||||
@@ -72,6 +85,13 @@ pub async fn list_tasks_for_frontend(
|
||||
.transpose()?,
|
||||
frontend_subscriber_id: raw.frontend_subscriber_id,
|
||||
frontend_subscriber_payload: raw.frontend_subscriber_payload,
|
||||
on_complete_primary_media_file_token: raw.on_complete_primary_media_file_token.map(|t| MediaFileToken::new_from_str(&t)),
|
||||
on_complete_primary_media_file_class: raw.on_complete_primary_media_file_class
|
||||
.map(|c| TaskMediaFileClass::from_str(&c))
|
||||
.transpose()?,
|
||||
on_complete_batch_token: raw.on_complete_batch_token.map(|t| BatchGenerationToken::new_from_str(&t)),
|
||||
on_complete_primary_media_file_cdn_url: raw.on_complete_primary_media_file_cdn_url,
|
||||
on_complete_primary_media_file_thumbnail_url_template: raw.on_complete_primary_media_file_thumbnail_url_template,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
completed_at: raw.completed_at,
|
||||
@@ -93,6 +113,11 @@ struct TaskItemRaw {
|
||||
frontend_caller: Option<String>,
|
||||
frontend_subscriber_id: Option<String>,
|
||||
frontend_subscriber_payload: Option<String>,
|
||||
on_complete_primary_media_file_token: Option<String>,
|
||||
on_complete_primary_media_file_class: Option<String>,
|
||||
on_complete_batch_token: Option<String>,
|
||||
on_complete_primary_media_file_cdn_url: Option<String>,
|
||||
on_complete_primary_media_file_thumbnail_url_template: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
completed_at: Option<DateTime<Utc>>,
|
||||
|
||||
@@ -3,4 +3,5 @@ pub mod list_tasks_by_provider_and_status;
|
||||
pub mod list_tasks_by_provider_and_tokens;
|
||||
pub mod list_tasks_for_frontend;
|
||||
pub mod mark_task_as_dismissed;
|
||||
pub mod update_successful_task_status_with_metadata;
|
||||
pub mod update_task_status;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use crate::connection::TaskDbConnection;
|
||||
use crate::error::SqliteTasksError;
|
||||
use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
const SUCCESSFUL_STATUS : &str = TaskStatus::CompleteSuccess.to_str();
|
||||
|
||||
pub struct UpdateSuccessfulTaskArgs<'a> {
|
||||
pub db: &'a TaskDbConnection,
|
||||
pub task_id: &'a TaskId,
|
||||
pub maybe_batch_token: Option<&'a BatchGenerationToken>,
|
||||
pub maybe_primary_media_file_token: Option<&'a MediaFileToken>,
|
||||
pub maybe_primary_media_file_class: Option<TaskMediaFileClass>,
|
||||
pub maybe_primary_media_file_cdn_url: Option<&'a str>,
|
||||
pub maybe_primary_media_file_thumbnail_url_template: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Returns true if rows were updated.
|
||||
pub async fn update_successful_task_status_with_metadata(
|
||||
args: UpdateSuccessfulTaskArgs<'_>,
|
||||
) -> Result<bool, SqliteTasksError> {
|
||||
|
||||
// TODO(bt,2025-07-12): Fix this. The sqlx mysql queries never required temporaries
|
||||
let task_id_temp = args.task_id.as_str();
|
||||
let maybe_batch_token_temp = args.maybe_batch_token.map(|t| t.as_str());
|
||||
let maybe_primary_media_token_temp = args.maybe_primary_media_file_token.map(|t| t.as_str());
|
||||
let maybe_primary_media_class = args.maybe_primary_media_file_class.map(|c| c.to_str());
|
||||
|
||||
// TODO(bt,2025-07-15): We can't set a LIMIT without a certain compiler flag for SQLite ?
|
||||
let query = sqlx::query!(r#"
|
||||
UPDATE tasks
|
||||
SET
|
||||
task_status = ?,
|
||||
on_complete_batch_token = ?,
|
||||
on_complete_primary_media_file_token = ?,
|
||||
on_complete_primary_media_file_class = ?,
|
||||
on_complete_primary_media_file_cdn_url = ?,
|
||||
on_complete_primary_media_file_thumbnail_url_template = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
SUCCESSFUL_STATUS,
|
||||
maybe_batch_token_temp,
|
||||
maybe_primary_media_token_temp,
|
||||
maybe_primary_media_class,
|
||||
args.maybe_primary_media_file_cdn_url,
|
||||
args.maybe_primary_media_file_thumbnail_url_template,
|
||||
task_id_temp,
|
||||
);
|
||||
|
||||
// info!("query: {:?}", query.sql());
|
||||
|
||||
let res = query.execute(args.db.get_pool()).await?;
|
||||
let rows_updated = res.rows_affected() > 0;
|
||||
|
||||
Ok(rows_updated)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod task_media_file_class;
|
||||
pub mod task_model_type;
|
||||
pub mod task_status;
|
||||
pub mod task_type;
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::error::enum_error::EnumError;
|
||||
#[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 TaskMediaFileClass {
|
||||
/// Audio files: wav, mp3, etc.
|
||||
Audio,
|
||||
|
||||
/// Image files: png, jpeg, etc.
|
||||
Image,
|
||||
|
||||
/// Video files: mp4, etc.
|
||||
Video,
|
||||
|
||||
/// 3D engine data: glb, gltf, etc.
|
||||
Dimensional,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(TaskMediaFileClass);
|
||||
//impl_mysql_enum_coders!(TaskMediaFileType);
|
||||
//impl_mysql_from_row!(TaskMediaFileType);
|
||||
|
||||
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
|
||||
|
||||
impl TaskMediaFileClass {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Audio => "audio",
|
||||
Self::Image => "image",
|
||||
Self::Video => "video",
|
||||
Self::Dimensional => "dimensional",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
||||
match value {
|
||||
"audio" => Ok(Self::Audio),
|
||||
"image" => Ok(Self::Image),
|
||||
"video" => Ok(Self::Video),
|
||||
"dimensional" => Ok(Self::Dimensional),
|
||||
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
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::Audio,
|
||||
Self::Image,
|
||||
Self::Video,
|
||||
Self::Dimensional,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(TaskMediaFileClass::Audio, "audio");
|
||||
assert_serialization(TaskMediaFileClass::Image, "image");
|
||||
assert_serialization(TaskMediaFileClass::Video, "video");
|
||||
assert_serialization(TaskMediaFileClass::Dimensional, "dimensional");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(TaskMediaFileClass::Audio.to_str(), "audio");
|
||||
assert_eq!(TaskMediaFileClass::Image.to_str(), "image");
|
||||
assert_eq!(TaskMediaFileClass::Video.to_str(), "video");
|
||||
assert_eq!(TaskMediaFileClass::Dimensional.to_str(), "dimensional");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(TaskMediaFileClass::from_str("audio").unwrap(), TaskMediaFileClass::Audio);
|
||||
assert_eq!(TaskMediaFileClass::from_str("image").unwrap(), TaskMediaFileClass::Image);
|
||||
assert_eq!(TaskMediaFileClass::from_str("video").unwrap(), TaskMediaFileClass::Video);
|
||||
assert_eq!(TaskMediaFileClass::from_str("dimensional").unwrap(), TaskMediaFileClass::Dimensional);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_err() {
|
||||
let result = TaskMediaFileClass::from_str("asdf");
|
||||
assert!(result.is_err());
|
||||
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
|
||||
assert_eq!(value, "asdf");
|
||||
} else {
|
||||
panic!("Expected EnumError::CouldNotConvertFromString");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = TaskMediaFileClass::all_variants();
|
||||
assert_eq!(variants.len(), 4);
|
||||
assert_eq!(variants.pop_first(), Some(TaskMediaFileClass::Audio));
|
||||
assert_eq!(variants.pop_first(), Some(TaskMediaFileClass::Image));
|
||||
assert_eq!(variants.pop_first(), Some(TaskMediaFileClass::Video));
|
||||
assert_eq!(variants.pop_first(), Some(TaskMediaFileClass::Dimensional));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(TaskMediaFileClass::all_variants().len(), TaskMediaFileClass::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in TaskMediaFileClass::all_variants() {
|
||||
// Test to_str(), from_str(), Display, and Debug.
|
||||
assert_eq!(variant, TaskMediaFileClass::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, TaskMediaFileClass::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, TaskMediaFileClass::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ impl Default for TaskStatus {
|
||||
}
|
||||
|
||||
impl TaskStatus {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
pub const fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Pending => "pending",
|
||||
Self::Started => "started",
|
||||
|
||||
@@ -42,11 +42,9 @@ memory_caching = { path = "../../../lib/caching/memory_caching" }
|
||||
migration = { path = "../../../lib/migration" }
|
||||
mimetypes = { path = "../../../lib/files/mimetypes" }
|
||||
mysql_queries = { path = "../../../schema/database/mysql_queries" }
|
||||
openai_sora_client = { path = "../../../api_clients/openai_sora_client" }
|
||||
primitives.workspace = true
|
||||
redis_common = { path = "../../../schema/database/redis_common" }
|
||||
redis_schema = { path = "../../../schema/database/redis_schema" }
|
||||
shared_service_components = { workspace = true }
|
||||
subprocess_common = { path = "../../../lib/subprocess_common" }
|
||||
thumbnail_generator = { path = "../../../api_clients/thumbnail_generator"}
|
||||
tokens.workspace = true
|
||||
|
||||
@@ -35,6 +35,8 @@ pub async fn main_loop(job_dependencies: JobDependencies) {
|
||||
let mut sort_by_priority_count = 0;
|
||||
|
||||
while !job_dependencies.job.system.application_shutdown.get() {
|
||||
info!("Job loop iteration starting...");
|
||||
|
||||
if let Some(pause_file) = job_dependencies.fs.maybe_pause_file.as_deref() {
|
||||
while file_exists(pause_file) {
|
||||
warn!("Pause file exists. Pausing until deleted: {:?}", pause_file);
|
||||
@@ -53,6 +55,8 @@ pub async fn main_loop(job_dependencies: JobDependencies) {
|
||||
|
||||
let batch_query_start_time = Instant::now();
|
||||
|
||||
info!("Querying jobs...");
|
||||
|
||||
let maybe_available_jobs = list_available_generic_inference_jobs(ListAvailableGenericInferenceJobArgs {
|
||||
num_records: job_dependencies.job.system.job_batch_size,
|
||||
is_debug_worker: false, // TODO
|
||||
@@ -117,6 +121,8 @@ pub async fn main_loop(job_dependencies: JobDependencies) {
|
||||
|
||||
error_timeout_millis = START_TIMEOUT_MILLIS; // reset
|
||||
|
||||
info!("Job loop batch processing complete. Sleeping {} ms", job_dependencies.job.system.job_batch_wait_millis);
|
||||
|
||||
std::thread::sleep(Duration::from_millis(job_dependencies.job.system.job_batch_wait_millis));
|
||||
}
|
||||
|
||||
@@ -192,6 +198,8 @@ async fn process_job_batch(job_dependencies: &JobDependencies, jobs: Vec<Availab
|
||||
job_duration.as_millis() as u64,
|
||||
&job_duration_instrumentation_attributes);
|
||||
}
|
||||
|
||||
info!("Process job batch loop ended.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::job::job_types::image_generation::v2::process_single_sora_job::process_single_sora_job;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::anyhow;
|
||||
@@ -21,7 +20,6 @@ use crate::job::job_types::f5_tts::process_single_f5_tts_job::process_single_f5_
|
||||
use crate::job::job_types::format_conversion::process_single_format_conversion_job::process_single_format_conversion_job;
|
||||
use crate::job::job_types::gpt_sovits::process_single_gpt_sovits_job::process_single_gpt_sovits_job;
|
||||
use crate::job::job_types::image_generation::process_single_ig_job::process_single_ig_job;
|
||||
use crate::job::job_types::image_generation::v2::process_single_sora_job;
|
||||
use crate::job::job_types::lipsync::process_single_lipsync_job::process_single_lipsync_job;
|
||||
use crate::job::job_types::mocap::process_single_mc_job::process_single_mc_job;
|
||||
use crate::job::job_types::render_engine_scene::process_render_engine_scene::process_single_render_engine_scene_job;
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod process_single_ig_job;
|
||||
pub mod sd;
|
||||
pub mod v2;
|
||||
pub mod sd;
|
||||
+2
-5
@@ -1,14 +1,13 @@
|
||||
use log::info;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
|
||||
use errors::anyhow;
|
||||
use log::info;
|
||||
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
|
||||
|
||||
use crate::job::job_loop::job_success_result::JobSuccessResult;
|
||||
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
|
||||
use crate::job::job_types::image_generation::sd::process_job::process_job_selection;
|
||||
use crate::job::job_types::image_generation::sd::process_job::StableDiffusionProcessArgs;
|
||||
use crate::job::job_types::image_generation::v2::process_single_sora_job::{process_single_sora_job, ImageGenerationProcessArgs};
|
||||
use crate::state::job_dependencies::JobDependencies;
|
||||
|
||||
pub async fn process_single_ig_job(job_dependencies: &JobDependencies, job: &AvailableInferenceJob) -> Result<JobSuccessResult, ProcessSingleJobError> {
|
||||
@@ -26,9 +25,7 @@ pub async fn process_single_ig_job(job_dependencies: &JobDependencies, job: &Ava
|
||||
|
||||
match job.job_type {
|
||||
InferenceJobType::ImageGenApi => {
|
||||
info!("Processing image job as ImageGenApi");
|
||||
let args = ImageGenerationProcessArgs { job_dependencies, job };
|
||||
process_single_sora_job(&args).await
|
||||
Err(ProcessSingleJobError::Other(anyhow!("ImageGenAPI (Sora) jobs are no longer supported")))
|
||||
},
|
||||
_ => {
|
||||
info!("Processing image job as **NOT** ImageGenApi. Type: {:?}", job.job_type);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod process_single_sora_job;
|
||||
-287
@@ -1,287 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bucket_paths::legacy::remote_file_manager_paths::media_descriptor::MediaImagePngDescriptor;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
|
||||
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_origin_category::MediaFileOriginCategory;
|
||||
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::media_files::media_file_type::MediaFileType;
|
||||
use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs;
|
||||
use mysql_queries::queries::media_files::create::generic_insert::insert_media_file_generic_from_job::{insert_media_file_generic_from_job, InsertFromJobArgs};
|
||||
|
||||
use crate::job::job_loop::job_success_result::{JobSuccessResult, ResultEntity};
|
||||
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
|
||||
use crate::state::job_dependencies::JobDependencies;
|
||||
use crate::util::extractors::get_polymorphic_args_from_job::get_polymorphic_args_from_job;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use cloud_storage::remote_file_manager::remote_cloud_file_manager::RemoteCloudFileClient;
|
||||
use composite_identifiers::by_table::batch_generations::batch_generation_entity::BatchGenerationEntity;
|
||||
use errors::AnyhowResult;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use hashing::sha256::sha256_hash_file::sha256_hash_file;
|
||||
use log::info;
|
||||
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
|
||||
use mysql_queries::queries::batch_generations::insert_batch_generation_records::{insert_batch_generation_records, InsertBatchArgs};
|
||||
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use openai_sora_client::recipes::wait_for_image_gen_status::wait_for_image_gen_status;
|
||||
use openai_sora_client::requests::image_gen::sora_job_status::{Generation, TaskStatus};
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use shared_service_components::sora_redis_credentials::get_sora_credentials_from_redis::get_sora_credentials_from_redis;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempdir::TempDir;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use url::Url;
|
||||
|
||||
const IMAGE_PREFIX : &str = "image_";
|
||||
const IMAGE_SUFFIX : &str = ".png"; // TODO: Vary based on filetype.
|
||||
|
||||
pub struct ImageGenerationProcessArgs<'a> {
|
||||
pub job_dependencies: &'a JobDependencies,
|
||||
pub job: &'a AvailableInferenceJob,
|
||||
}
|
||||
|
||||
pub struct ImageGenerationArgs {
|
||||
pub number_of_samples: u32,
|
||||
pub sora_task_id: String,
|
||||
}
|
||||
|
||||
pub async fn ig_args_from_job(args: &ImageGenerationProcessArgs<'_>) -> Result<ImageGenerationArgs, ProcessSingleJobError> {
|
||||
let polymorphic_args = get_polymorphic_args_from_job(&args.job)?;
|
||||
|
||||
let job_args = match polymorphic_args {
|
||||
PolymorphicInferenceArgs::Sg(args) => args,
|
||||
_ => {
|
||||
return Err(
|
||||
ProcessSingleJobError::from_anyhow_error(anyhow!("wrong inner args for job!"))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let number_of_samples = job_args.maybe_number_of_samples.unwrap_or(1);
|
||||
let sora_task_id = job_args.maybe_sora_task_id.clone().ok_or_else(|| ProcessSingleJobError::from_anyhow_error(anyhow!("Missing Sora Task ID")))?;
|
||||
|
||||
Ok(ImageGenerationArgs {
|
||||
number_of_samples,
|
||||
sora_task_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn process_single_sora_job(args: &ImageGenerationProcessArgs<'_>) -> Result<JobSuccessResult, ProcessSingleJobError> {
|
||||
let job = args.job;
|
||||
let deps = args.job_dependencies;
|
||||
let mysql_pool = &deps.db.mysql_pool;
|
||||
|
||||
let ig_args = ig_args_from_job(&args).await?;
|
||||
|
||||
let _job_progress_reporter = args.job_dependencies.clients.job_progress_reporter
|
||||
.new_generic_inference(job.inference_job_token.as_str())
|
||||
.map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?;
|
||||
|
||||
//==================== TEMP DIR ==================== //
|
||||
let work_temp_dir = format!("temp_image_generation_{}", job.id.0);
|
||||
|
||||
//NB: TempDir exists until it goes out of scope, at which point it should delete from filesystem.
|
||||
let work_temp_dir = args.job_dependencies.fs.scoped_temp_dir_creator_for_work
|
||||
.new_tempdir(&work_temp_dir)
|
||||
.map_err(|e| ProcessSingleJobError::from_io_error(e))?;
|
||||
|
||||
// thread::sleep(seconds) to check the directory
|
||||
|
||||
let inference_start_time = Instant::now();
|
||||
|
||||
let redis_pool = deps
|
||||
.db
|
||||
.maybe_keepalive_redis_pool
|
||||
.as_ref()
|
||||
.ok_or_else(|| ProcessSingleJobError::Other(anyhow!("failed to get redis pool")))?;
|
||||
|
||||
let redis = &mut redis_pool
|
||||
.get()
|
||||
.map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?;
|
||||
|
||||
let sora_credentials = get_sora_credentials_from_redis(redis)?;
|
||||
|
||||
let sora_task_id = ig_args.sora_task_id.clone();
|
||||
|
||||
let sora_task_response = wait_for_image_gen_status(
|
||||
&sora_task_id,
|
||||
&sora_credentials,
|
||||
None,
|
||||
).await.map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?;
|
||||
|
||||
match TaskStatus::from_str(&sora_task_response.status) {
|
||||
TaskStatus::Succeeded => {} // Success case. Fall-through.
|
||||
TaskStatus::Failed => {
|
||||
return Err(ProcessSingleJobError::Other(anyhow!("Task failed: {:?}", sora_task_response)));
|
||||
}
|
||||
_ => {
|
||||
return Err(ProcessSingleJobError::Other(anyhow!("Task not completed yet: {:?}", sora_task_response)));
|
||||
}
|
||||
}
|
||||
|
||||
let generations = sora_task_response.generations;
|
||||
|
||||
if generations.is_empty() {
|
||||
return Err(ProcessSingleJobError::Other(anyhow!("No generations found in task response")));
|
||||
}
|
||||
|
||||
let inference_duration = Instant::now().duration_since(inference_start_time);
|
||||
|
||||
let remote_cloud_file_client = &deps.buckets.public_bucket_client;
|
||||
|
||||
let mut entries = vec![];
|
||||
|
||||
let batch_token = BatchGenerationToken::generate();
|
||||
let mut maybe_first_media_file_token = None;
|
||||
|
||||
for (i, generation) in generations.iter().enumerate() {
|
||||
info!("Downloading generation {} of {} from Sora task ID: {}", i + 1, generations.len(), &sora_task_id);
|
||||
|
||||
let download_path = download_generation(&generation, &work_temp_dir).await?;
|
||||
|
||||
info!("Sora image was downloaded to filesystem path: {:?}", &download_path);
|
||||
|
||||
info!("Reading file bytes and metadata...");
|
||||
|
||||
let bytes = file_read_bytes(&download_path)?;
|
||||
let sha256_checksum = sha256_hash_file(&download_path)?;
|
||||
let mimetype: &str = get_mimetype_for_bytes(&bytes).unwrap_or("application/octet-stream");
|
||||
|
||||
let media_file_bucket_path =
|
||||
MediaFileBucketPath::generate_new(Some(IMAGE_PREFIX), Some(IMAGE_SUFFIX));
|
||||
|
||||
remote_cloud_file_client.upload_filename_with_content_type(
|
||||
media_file_bucket_path.to_full_object_pathbuf(),
|
||||
&download_path,
|
||||
mimetype,
|
||||
).await?;
|
||||
|
||||
// TODO(ks: 2025-04-08): This should insert the media file token and maybe additional metadata about the generation
|
||||
let (media_file_token, _id) = insert_media_file_generic_from_job(InsertFromJobArgs {
|
||||
pool: mysql_pool,
|
||||
job,
|
||||
media_class: MediaFileClass::Image,
|
||||
media_type: MediaFileType::Png, // TODO(bt,2024-04-30): Verify that these are png images.
|
||||
origin_category: MediaFileOriginCategory::Inference,
|
||||
origin_product_category: MediaFileOriginProductCategory::ImageGeneration,
|
||||
maybe_origin_model_type: Some(MediaFileOriginModelType::StorytellerStudioImageGen),
|
||||
maybe_origin_model_token: None,
|
||||
maybe_origin_filename: None, // TODO: Need to grab this
|
||||
maybe_batch_token: Some(&batch_token),
|
||||
maybe_prompt_token: None,
|
||||
maybe_mime_type: Some(mimetype.as_ref()),
|
||||
file_size_bytes: bytes.len() as u64,
|
||||
maybe_duration_millis: Some(inference_duration.as_millis() as u64),
|
||||
maybe_audio_encoding: None,
|
||||
maybe_video_encoding: None,
|
||||
maybe_frame_width: Some(1024), // TODO: Need to grab this
|
||||
maybe_frame_height: Some(1024), // TODO: Need to grab this
|
||||
checksum_sha2: sha256_checksum.as_str(),
|
||||
maybe_text_transcript: None,
|
||||
public_bucket_directory_hash: media_file_bucket_path.get_object_hash(),
|
||||
maybe_public_bucket_prefix: media_file_bucket_path.get_optional_prefix(),
|
||||
maybe_public_bucket_extension: media_file_bucket_path.get_optional_extension(),
|
||||
maybe_extra_media_info: None,
|
||||
maybe_creator_file_synthetic_id_category: IdCategory::MediaFile,
|
||||
maybe_creator_category_synthetic_id_category: IdCategory::MediaFile,
|
||||
maybe_mod_user_token: None,
|
||||
is_generated_on_prem: false,
|
||||
generated_by_worker: Some(&args.job_dependencies.job.info.container.hostname),
|
||||
generated_by_cluster: Some(&args.job_dependencies.job.info.container.cluster_name),
|
||||
maybe_title: None,
|
||||
maybe_scene_source_media_file_token: None,
|
||||
is_intermediate_system_file: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
if maybe_first_media_file_token.is_none() {
|
||||
maybe_first_media_file_token = Some(media_file_token.clone());
|
||||
}
|
||||
|
||||
let batch_generation_entity: BatchGenerationEntity = BatchGenerationEntity::MediaFile(
|
||||
media_file_token
|
||||
);
|
||||
|
||||
entries.push(batch_generation_entity);
|
||||
}
|
||||
|
||||
// TODO(bt,2024-02-22): This transaction should wrap everything
|
||||
let mut transaction = mysql_pool.begin()
|
||||
.await
|
||||
.map_err(|e| ProcessSingleJobError::from_anyhow_error(anyhow!(e)))?;
|
||||
|
||||
let batch_token_result = insert_batch_generation_records(InsertBatchArgs {
|
||||
entries,
|
||||
maybe_existing_batch_token: Some(&batch_token),
|
||||
transaction: &mut transaction,
|
||||
}).await;
|
||||
|
||||
let _batch_token = match batch_token_result {
|
||||
Ok(v) => { v.to_string() }
|
||||
Err(_err) => {
|
||||
return Err(
|
||||
ProcessSingleJobError::from_anyhow_error(
|
||||
anyhow!("No batch token? something has failed.")
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// NB: Don't fail the job if the query fails.
|
||||
// let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
// maybe_apriori_prompt_token: Some(&prompt_token),
|
||||
// prompt_type: PromptType::StableDiffusion,
|
||||
// maybe_creator_user_token: job.maybe_creator_user_token_typed.as_ref(),
|
||||
// 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
|
||||
// creator_ip_address: &job.creator_ip_address,
|
||||
// mysql_executor: &mut *transaction,
|
||||
// phantom: Default::default(),
|
||||
// }).await;
|
||||
|
||||
// match prompt_result {
|
||||
// Ok(_token) => {}
|
||||
// Err(err) => {
|
||||
// error!("No prompt result token? something has failed: {:?}", err);
|
||||
// return Err(ProcessSingleJobError::from_anyhow_error(anyhow!("No prompt result token? something has failed.")));
|
||||
// }
|
||||
// }
|
||||
|
||||
let _r = transaction.commit().await.map_err(|e| ProcessSingleJobError::from_anyhow_error(anyhow!(e)))?;
|
||||
|
||||
// TODO(bt,2024-02-12): Return the batch token instead. (We're not ready for that.)
|
||||
Ok(JobSuccessResult {
|
||||
inference_duration,
|
||||
maybe_result_entity: maybe_first_media_file_token.map(|media_file_token| ResultEntity {
|
||||
entity_type: InferenceResultType::MediaFile,
|
||||
entity_token: media_file_token.to_string()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn download_generation(generation: &Generation, tempdir: &TempDir) -> AnyhowResult<PathBuf> {
|
||||
let url = Url::parse(&generation.url)?;
|
||||
|
||||
let response = reqwest::get(&generation.url).await?;
|
||||
let image_bytes = response.bytes().await?;
|
||||
|
||||
let ext = url.path().split(".").last().unwrap_or("png");
|
||||
let download_filename = format!("{}.{}", generation.id, ext);
|
||||
let download_path = tempdir.path().join(download_filename);
|
||||
|
||||
let mut file = File::create(&download_path)?;
|
||||
file.write_all(&image_bytes)?;
|
||||
|
||||
Ok(download_path)
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { PopoverMenu } from "@storyteller/ui-popover";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faListCheck,
|
||||
faSpinnerThird,
|
||||
faXmark,
|
||||
faTrashAlt,
|
||||
faTasks,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import {
|
||||
galleryModalLightboxMediaId,
|
||||
galleryModalLightboxVisible,
|
||||
galleryModalLightboxImage,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import type { GalleryItem } from "@storyteller/ui-gallery-modal";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { GetTaskQueue, MarkTaskAsDismissed } from "@storyteller/tauri-api";
|
||||
import type { TaskQueueItem } from "@storyteller/tauri-api";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import {
|
||||
useSelectedImageModel,
|
||||
useSelectedVideoModel,
|
||||
ModelPage,
|
||||
} from "@storyteller/ui-model-selector";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import {
|
||||
getProviderDisplayName,
|
||||
getModelDisplayName,
|
||||
} from "@storyteller/model-list";
|
||||
import { CloseButton } from "@storyteller/ui-close-button";
|
||||
import { ActionReminderModal } from "@storyteller/ui-action-reminder-modal";
|
||||
import { TaskMediaFileClass } from "@storyteller/api-enums";
|
||||
|
||||
type InProgressTask = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
progress: number;
|
||||
updatedAt?: Date;
|
||||
canDismiss?: boolean;
|
||||
};
|
||||
|
||||
type CompletedTask = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
thumbnailUrl?: string;
|
||||
completedAt?: Date;
|
||||
updatedAt?: Date;
|
||||
imageUrls?: string[];
|
||||
mediaTokens?: string[];
|
||||
mediaFileClass?: TaskMediaFileClass;
|
||||
};
|
||||
|
||||
const InProgressCard = ({
|
||||
task,
|
||||
onDismiss,
|
||||
}: {
|
||||
task: InProgressTask;
|
||||
onDismiss?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-2 rounded-md border border-ui-divider bg-ui-background p-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded bg-ui-controls">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="text-base-fg/60 animate-spin"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="text-base-fg/90 truncate font-medium">
|
||||
{task.title}
|
||||
</div>
|
||||
<div className="text-base-fg/60 ml-2 shrink-0 text-xs tabular-nums">
|
||||
{Math.max(0, Math.min(100, Math.round(task.progress)))}%
|
||||
</div>
|
||||
</div>
|
||||
{task.subtitle && (
|
||||
<div className="text-base-fg mt-0.5 truncate text-xs opacity-60">
|
||||
{task.subtitle}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 h-1.5 w-full rounded bg-ui-controls">
|
||||
<div
|
||||
className="h-1.5 rounded bg-brand-primary-400"
|
||||
style={{ width: `${Math.max(0, Math.min(100, task.progress))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="text-base-fg/60 ml-auto h-6 w-6 rounded-full p-1 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CompletedCard = ({
|
||||
task,
|
||||
onClick,
|
||||
onDismiss,
|
||||
}: {
|
||||
task: CompletedTask;
|
||||
onClick?: () => void;
|
||||
onDismiss?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="mb-2 flex cursor-pointer items-center gap-2.5 rounded-md border border-ui-divider bg-ui-background p-2 transition-colors hover:bg-ui-controls/40"
|
||||
onClick={onClick}
|
||||
role={onClick ? "button" : undefined}
|
||||
tabIndex={onClick ? 0 : -1}
|
||||
>
|
||||
<div className="h-14 w-14 shrink-0 overflow-hidden rounded bg-ui-controls">
|
||||
{task.thumbnailUrl ? (
|
||||
<img
|
||||
src={task.thumbnailUrl}
|
||||
alt={task.title}
|
||||
onError={(e) => {
|
||||
console.log("Failed to load thumbnail", e);
|
||||
let errorPlaceholder = "/resources/placeholders/placeholder.png";
|
||||
switch (task.mediaFileClass) {
|
||||
case TaskMediaFileClass.Video:
|
||||
errorPlaceholder = "/resources/placeholders/placeholder_play.png";
|
||||
break;
|
||||
}
|
||||
e.currentTarget.src = errorPlaceholder;
|
||||
}}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-base-fg/40 flex h-full w-full items-center justify-center text-[10px]">
|
||||
Done
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-base-fg/90 truncate text-sm font-medium">
|
||||
{task.title}
|
||||
</div>
|
||||
{task.subtitle && (
|
||||
<div className="text-base-fg mt-0.5 truncate text-xs opacity-60">
|
||||
{task.subtitle}
|
||||
</div>
|
||||
)}
|
||||
{task.completedAt && (
|
||||
<div className="text-base-fg text-xs opacity-60">
|
||||
{task.completedAt.toISOString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="text-base-fg/60 ml-auto h-6 w-6 rounded-full p-1 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TaskQueue = () => {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [inProgress, setInProgress] = useState<InProgressTask[]>([]);
|
||||
const [completed, setCompleted] = useState<CompletedTask[]>([]);
|
||||
const [lastReadAt, setLastReadAt] = useState<number>(() => {
|
||||
const stored = localStorage.getItem("taskQueueLastReadAt");
|
||||
return stored ? parseInt(stored, 10) : 0;
|
||||
});
|
||||
|
||||
// remove unread state; unread tracking handled via IDs below
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [unreadCompletedIds, setUnreadCompletedIds] = useState<string[]>([]);
|
||||
const prevCompletedIdsRef = useRef<Set<string>>(new Set());
|
||||
// confirmation modal state for destructive actions
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [afterConfirm, setAfterConfirm] = useState<(() => void) | null>(null);
|
||||
|
||||
// Use currently selected models for image and video pages to drive fake progress.
|
||||
const selectedImageModel = useSelectedImageModel(ModelPage.TextToImage);
|
||||
const selectedVideoModel = useSelectedVideoModel(ModelPage.ImageToVideo);
|
||||
// Snapshot per-task duration so switching models doesn't affect existing items
|
||||
const taskDurationRef = useRef<Map<string, number>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const formatTitleParts = (t: TaskQueueItem) => {
|
||||
const provider = t.provider
|
||||
? getProviderDisplayName(String(t.provider).toLowerCase())
|
||||
: undefined;
|
||||
const taskTypeStr = t.task_type ? String(t.task_type) : "";
|
||||
const kind = taskTypeStr.includes("video")
|
||||
? "Video"
|
||||
: taskTypeStr.includes("image")
|
||||
? "Image"
|
||||
: undefined;
|
||||
const modelDisplay = t.model_type
|
||||
? getModelDisplayName(String(t.model_type))
|
||||
: undefined;
|
||||
|
||||
const title = kind || "Task";
|
||||
const subtitle =
|
||||
modelDisplay && provider
|
||||
? `${modelDisplay} — ${provider}`
|
||||
: modelDisplay || provider || undefined;
|
||||
return { title, subtitle, kind };
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const result = await GetTaskQueue();
|
||||
|
||||
if (cancelled) return;
|
||||
console.log("TaskQueue:GetTaskQueue result", result);
|
||||
|
||||
const { tasks } = result;
|
||||
|
||||
const now = Date.now();
|
||||
const inProg = tasks
|
||||
.filter(
|
||||
(t) => t.task_status === "pending" || t.task_status === "started",
|
||||
)
|
||||
.sort((a, b) => b.updated_at.getTime() - a.updated_at.getTime())
|
||||
.map((t: TaskQueueItem) => {
|
||||
const createdMs = t.created_at.getTime();
|
||||
const taskTypeStr = t.task_type
|
||||
? String(t.task_type).toLowerCase()
|
||||
: "";
|
||||
const isVideo = taskTypeStr.includes("video");
|
||||
let duration = taskDurationRef.current.get(t.id);
|
||||
if (!duration) {
|
||||
duration =
|
||||
(isVideo
|
||||
? selectedVideoModel?.progressBarTime
|
||||
: selectedImageModel?.progressBarTime) ?? 20000;
|
||||
taskDurationRef.current.set(t.id, duration);
|
||||
}
|
||||
const raw = ((now - createdMs) / duration) * 100;
|
||||
const progress = Math.min(95, Math.max(0, raw));
|
||||
const parts = formatTitleParts(t);
|
||||
const canDismiss = now - createdMs > 5 * 60 * 1000; // 5 minutes
|
||||
return {
|
||||
id: t.id,
|
||||
title: `Generating ${parts.kind || "Task"}...`,
|
||||
subtitle: parts.subtitle,
|
||||
progress,
|
||||
updatedAt: t.updated_at,
|
||||
canDismiss,
|
||||
};
|
||||
});
|
||||
|
||||
// prune durations for tasks no longer in progress
|
||||
const inProgIds = new Set(inProg.map((x) => x.id));
|
||||
for (const id of Array.from(taskDurationRef.current.keys())) {
|
||||
if (!inProgIds.has(id)) {
|
||||
taskDurationRef.current.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
const done = tasks
|
||||
.filter((t) => t.task_status === "complete_success")
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.completed_at?.getTime() || b.updated_at.getTime()) -
|
||||
(a.completed_at?.getTime() || a.updated_at.getTime()),
|
||||
)
|
||||
.map((t: TaskQueueItem) => ({
|
||||
id: t.id,
|
||||
...formatTitleParts(t),
|
||||
thumbnailUrl: t.completed_item?.primary_media_file?.maybe_thumbnail_url_template ?
|
||||
t.completed_item?.primary_media_file?.maybe_thumbnail_url_template.replace("{WIDTH}", "250") :
|
||||
undefined,
|
||||
imageUrls: t.completed_item?.primary_media_file?.cdn_url ?
|
||||
[t.completed_item?.primary_media_file?.cdn_url] :
|
||||
[],
|
||||
mediaTokens: t.completed_item?.primary_media_file?.token ?
|
||||
[t.completed_item?.primary_media_file?.token] :
|
||||
[],
|
||||
completedAt: t.completed_at,
|
||||
updatedAt: t.updated_at,
|
||||
}));
|
||||
|
||||
setInProgress(inProg);
|
||||
setCompleted(done);
|
||||
|
||||
// Track newly completed IDs when popover is closed
|
||||
const newCompletedIdSet = new Set(done.map((d) => d.id));
|
||||
const newlyCompletedIds: string[] = [];
|
||||
newCompletedIdSet.forEach((id) => {
|
||||
if (!prevCompletedIdsRef.current.has(id)) {
|
||||
newlyCompletedIds.push(id);
|
||||
}
|
||||
});
|
||||
prevCompletedIdsRef.current = newCompletedIdSet;
|
||||
if (!isPopoverOpen && newlyCompletedIds.length > 0) {
|
||||
setUnreadCompletedIds((prev) =>
|
||||
Array.from(new Set([...(prev ?? []), ...newlyCompletedIds])),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
const id = setInterval(load, 5000);
|
||||
let unlisten: Promise<UnlistenFn> | null = null;
|
||||
(async () => {
|
||||
// Update immediately when Tauri signals a generation completion
|
||||
unlisten = listen("generation-complete-event", () => {
|
||||
if (!cancelled) {
|
||||
load();
|
||||
}
|
||||
});
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(id);
|
||||
if (unlisten) {
|
||||
unlisten.then((f) => f());
|
||||
}
|
||||
};
|
||||
}, [
|
||||
lastReadAt,
|
||||
selectedImageModel?.progressBarTime,
|
||||
selectedVideoModel?.progressBarTime,
|
||||
isPopoverOpen,
|
||||
]);
|
||||
|
||||
const hasNothing = useMemo(
|
||||
() => inProgress.length === 0 && completed.length === 0,
|
||||
[inProgress.length, completed.length],
|
||||
);
|
||||
|
||||
const inProgressCount = inProgress.length;
|
||||
const badgeCount = inProgressCount + (unreadCompletedIds?.length ?? 0);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setIsPopoverOpen(open);
|
||||
if (open) {
|
||||
const now = Date.now();
|
||||
setLastReadAt(now);
|
||||
localStorage.setItem("taskQueueLastReadAt", String(now));
|
||||
setUnreadCompletedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const dismissTask = async (id: string) => {
|
||||
try {
|
||||
await MarkTaskAsDismissed(id);
|
||||
setInProgress((prev) => prev.filter((t) => t.id !== id));
|
||||
setCompleted((prev) => prev.filter((t) => t.id !== id));
|
||||
setUnreadCompletedIds((prev) => (prev ?? []).filter((x) => x !== id));
|
||||
taskDurationRef.current.delete(id);
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const dismissCompleted = async () => {
|
||||
const ids = completed.map((t) => t.id);
|
||||
try {
|
||||
await Promise.all(ids.map((id) => MarkTaskAsDismissed(id)));
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} finally {
|
||||
setCompleted([]);
|
||||
setUnreadCompletedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip content="Task Queue" position="bottom" closeOnClick={true}>
|
||||
<div className="relative">
|
||||
{badgeCount > 0 && (
|
||||
<div className="absolute -right-1 -top-1 z-20 flex h-[17px] w-[17px] items-center justify-center rounded-full bg-brand-primary-400 text-[13px] font-medium text-white">
|
||||
{badgeCount}
|
||||
</div>
|
||||
)}
|
||||
<PopoverMenu
|
||||
mode="default"
|
||||
buttonClassName="h-[38px] w-[38px] !p-0 relative"
|
||||
panelClassName="w-[360px] p-2 bg-ui-panel mt-2.5"
|
||||
position="bottom"
|
||||
align="end"
|
||||
triggerIcon={
|
||||
inProgressCount > 0 ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faListCheck} />
|
||||
)
|
||||
}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
{(close) => (
|
||||
<>
|
||||
<div className="flex max-h-[480px] flex-col">
|
||||
<div className="max-h-[420px] overflow-y-auto p-1">
|
||||
{hasNothing ? (
|
||||
<div className="text-base-fg/60 flex w-full flex-col items-center justify-center p-5">
|
||||
<div className="flex items-center gap-2.5 text-sm opacity-60">
|
||||
<FontAwesomeIcon icon={faTasks} /> No tasks yet
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{inProgress.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="text-base-fg/50 mb-1 px-1 text-xs uppercase tracking-wide">
|
||||
In Progress
|
||||
</div>
|
||||
{inProgress.map((t) => (
|
||||
<InProgressCard
|
||||
key={t.id}
|
||||
task={t}
|
||||
onDismiss={
|
||||
t.canDismiss
|
||||
? () => dismissTask(t.id)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{completed.length > 0 && (
|
||||
<div>
|
||||
<div className="text-base-fg/50 mb-1 px-1 text-xs uppercase tracking-wide">
|
||||
Completed
|
||||
</div>
|
||||
{completed.map((t) => (
|
||||
<CompletedCard
|
||||
key={t.id}
|
||||
task={t}
|
||||
onClick={() => {
|
||||
const firstMediaToken =
|
||||
t.mediaTokens?.[0] || t.id;
|
||||
const item: GalleryItem = {
|
||||
id: firstMediaToken,
|
||||
label: t.title,
|
||||
thumbnail: t.thumbnailUrl || null,
|
||||
fullImage: t.imageUrls?.[0] || null,
|
||||
createdAt: (t.completedAt || new Date()).toISOString(),
|
||||
mediaClass: "image",
|
||||
} as GalleryItem;
|
||||
galleryModalLightboxMediaId.value = item.id;
|
||||
galleryModalLightboxImage.value = {
|
||||
...item,
|
||||
imageUrls: t.imageUrls,
|
||||
mediaTokens: t.mediaTokens,
|
||||
} as unknown as GalleryItem;
|
||||
galleryModalLightboxVisible.value = true;
|
||||
close();
|
||||
}}
|
||||
onDismiss={() => dismissTask(t.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button
|
||||
className="grow"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setModalOpen(true);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Show all
|
||||
</Button>
|
||||
<Tooltip
|
||||
content="Clear all"
|
||||
position="bottom"
|
||||
closeOnClick={true}
|
||||
>
|
||||
<Button
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md bg-red/20 text-white hover:bg-red/40"
|
||||
aria-label="Clear all"
|
||||
onClick={() => {
|
||||
setAfterConfirm(() => () => close());
|
||||
setConfirmOpen(true);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PopoverMenu>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
className="h-[520px] max-w-3xl"
|
||||
showClose={false}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="rounded-t-xl border-ui-panel-border bg-ui-panel">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Task Queue</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex h-9 items-center justify-center bg-red/20 px-3 text-white hover:bg-red/40"
|
||||
onClick={() => {
|
||||
setAfterConfirm(() => null);
|
||||
setConfirmOpen(true);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className="mr-1" />
|
||||
Clear all
|
||||
</Button>
|
||||
<CloseButton onClick={() => setModalOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{hasNothing ? (
|
||||
<div className="text-base-fg/60 flex w-full flex-col items-center justify-center p-5">
|
||||
<div className="flex items-center gap-2.5 text-sm opacity-60">
|
||||
<FontAwesomeIcon icon={faTasks} /> No tasks yet
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{inProgress.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="text-base-fg/50 mb-2 px-1 text-xs uppercase tracking-wide">
|
||||
In Progress
|
||||
</div>
|
||||
{inProgress.map((t) => (
|
||||
<InProgressCard
|
||||
key={t.id}
|
||||
task={t}
|
||||
onDismiss={
|
||||
t.canDismiss ? () => dismissTask(t.id) : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{completed.length > 0 && (
|
||||
<div>
|
||||
<div className="text-base-fg/50 mb-2 px-1 text-xs uppercase tracking-wide">
|
||||
Completed
|
||||
</div>
|
||||
{completed.map((t) => (
|
||||
<CompletedCard
|
||||
key={t.id}
|
||||
task={t}
|
||||
onClick={() => {
|
||||
const item: GalleryItem = {
|
||||
id: t.id,
|
||||
label: t.title,
|
||||
thumbnail: t.thumbnailUrl || null,
|
||||
fullImage: t.imageUrls?.[0] || null,
|
||||
createdAt: (t.completedAt || new Date()).toISOString(),
|
||||
mediaClass: t.mediaFileClass,
|
||||
} as GalleryItem;
|
||||
galleryModalLightboxMediaId.value = item.id;
|
||||
galleryModalLightboxImage.value = item as GalleryItem;
|
||||
galleryModalLightboxVisible.value = true;
|
||||
setModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Confirm clear completed modal */}
|
||||
<ActionReminderModal
|
||||
isOpen={confirmOpen}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
title="Clear all tasks?"
|
||||
message={
|
||||
<span className="text-sm text-white/80">
|
||||
This will remove all completed tasks from the task queue.
|
||||
</span>
|
||||
}
|
||||
onPrimaryAction={async () => {
|
||||
await dismissCompleted();
|
||||
setConfirmOpen(false);
|
||||
if (afterConfirm) afterConfirm();
|
||||
setAfterConfirm(null);
|
||||
}}
|
||||
primaryActionText="Clear completed"
|
||||
secondaryActionText="Cancel"
|
||||
primaryActionIcon={faTrashAlt}
|
||||
primaryActionBtnClassName="bg-red hover:bg-red/80"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskQueue;
|
||||
@@ -62,6 +62,7 @@ import { useSubscriptionPlanChangedEvent } from "@storyteller/tauri-events";
|
||||
import { useCreditsState } from "@storyteller/credits";
|
||||
import { useSubscriptionState } from "@storyteller/subscription";
|
||||
import { UploadImagesButton } from "./UploadImagesButton";
|
||||
import TaskQueue from "./TaskQueue";
|
||||
|
||||
interface Props {
|
||||
pageName: string;
|
||||
@@ -223,6 +224,22 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTurnIntoVideoFromGallery = async (
|
||||
url: string,
|
||||
mediaId?: string,
|
||||
) => {
|
||||
try {
|
||||
topNavMediaId.value = mediaId || "";
|
||||
topNavMediaUrl.value = url;
|
||||
useTabStore.getState().setActiveTab("VIDEO");
|
||||
galleryModalVisibleViewMode.value = false;
|
||||
galleryModalVisibleDuringDrag.value = false;
|
||||
galleryModalLightboxVisible.value = false;
|
||||
} catch (e) {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
const getPageTitle = (): string => {
|
||||
switch (tabStore.activeTabId) {
|
||||
case "2D":
|
||||
@@ -449,7 +466,8 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<Activity />
|
||||
{/* <Activity /> */}
|
||||
<TaskQueue />
|
||||
</div>
|
||||
|
||||
<div className="no-drag">
|
||||
@@ -499,6 +517,7 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
mode="view"
|
||||
onDownloadClicked={downloadFile}
|
||||
onEditClicked={handleEditFromGallery}
|
||||
onTurnIntoVideoClicked={handleTurnIntoVideoFromGallery}
|
||||
/>
|
||||
|
||||
<ProviderSetupModal />
|
||||
|
||||
@@ -369,7 +369,7 @@ const PageDraw = () => {
|
||||
input.click();
|
||||
}}
|
||||
onDelete={(): void => {
|
||||
store.RESET();
|
||||
store.deleteSelectedItems();
|
||||
}}
|
||||
activeToolId={store.activeTool}
|
||||
currentShape={store.currentShape}
|
||||
|
||||
@@ -196,7 +196,6 @@ interface SceneState {
|
||||
|
||||
// Action for deleting selected items
|
||||
deleteSelectedItems: () => void;
|
||||
RESET: () => void;
|
||||
|
||||
// Clipboard actions
|
||||
copySelectedItems: () => void;
|
||||
@@ -235,8 +234,10 @@ interface SceneState {
|
||||
sendBackward: (nodeIds: string[]) => void;
|
||||
|
||||
// Start background removal (enqueues task with Tauri)
|
||||
beginRemoveBackground: (nodeIds: string[]) => Promise<void>;
|
||||
|
||||
beginRemoveBackground: (
|
||||
nodeIds: string[],
|
||||
) => Promise<void>;
|
||||
|
||||
// Finish background removal (handoff back from a Tauri-sent event)
|
||||
finishRemoveBackground: (
|
||||
nodeId: string,
|
||||
@@ -272,7 +273,7 @@ const getNextZIndex = (nodes: Node[], lineNodes: LineNode[]): number => {
|
||||
return allZIndices.length > 0 ? Math.max(...allZIndices) + 1 : 1;
|
||||
};
|
||||
|
||||
export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
// Initial state
|
||||
nodes: [],
|
||||
lineNodes: [],
|
||||
@@ -288,7 +289,7 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
fillColor: "white",
|
||||
currentShape: null,
|
||||
shapeColor: "#4d79b3",
|
||||
|
||||
|
||||
// Cursor initial state
|
||||
cursorPosition: null,
|
||||
cursorVisible: false,
|
||||
@@ -336,6 +337,7 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
updateNode: (
|
||||
id: string,
|
||||
updates: Partial<Node>,
|
||||
@@ -836,10 +838,6 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
}
|
||||
},
|
||||
|
||||
RESET: () => {
|
||||
set(store.getInitialState());
|
||||
},
|
||||
|
||||
// Clipboard actions
|
||||
copySelectedItems: () => {
|
||||
set((state) => {
|
||||
@@ -1439,7 +1437,9 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
beginRemoveBackground: async (nodeIds: string[]) => {
|
||||
beginRemoveBackground: async (
|
||||
nodeIds: string[],
|
||||
) => {
|
||||
const hasImageNodes = nodeIds.some((id) => {
|
||||
const node = get().nodes.find((n) => n.id === id);
|
||||
return node ? node.type === "image" : false;
|
||||
@@ -1455,7 +1455,7 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
}
|
||||
try {
|
||||
const base64Image = await convertFileToBase64(firstNode.imageFile);
|
||||
// NB: This handler is async, so we subscribe (externally) and
|
||||
// NB: This handler is async, so we subscribe (externally) and
|
||||
// wait for the bg removal to complete.
|
||||
const _response = await EnqueueImageBgRemoval({
|
||||
base64_image: base64Image,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./task_media_file_class.js";
|
||||
export * from "./task_model_type.js";
|
||||
export * from "./task_status.js";
|
||||
export * from "./task_type.js";
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
|
||||
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
|
||||
|
||||
export enum TaskMediaFileClass {
|
||||
Audio = "audio",
|
||||
Image = "image",
|
||||
Video = "video",
|
||||
Dimensional = "dimensional",
|
||||
}
|
||||
@@ -94,6 +94,28 @@ export class MediaFilesApi extends ApiManager {
|
||||
});
|
||||
}
|
||||
|
||||
public async GetMediaFilesByBatchToken({
|
||||
batchToken,
|
||||
}: {
|
||||
batchToken: string;
|
||||
}): Promise<ApiResponse<MediaFile[]>> {
|
||||
const endpoint = `${this.getApiSchemeAndHost()}/v1/media_files/batch/${batchToken}`;
|
||||
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
results: MediaFile[];
|
||||
pagination?: Pagination;
|
||||
error_reason?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: (response.results ?? []) as unknown as MediaFile[],
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public async GetMediaFileByToken({
|
||||
mediaFileToken,
|
||||
}: {
|
||||
|
||||
@@ -26,7 +26,7 @@ interface GalleryDraggableItemProps {
|
||||
activeFilter: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
onImageError: () => void;
|
||||
onImageError: (e: React.SyntheticEvent<HTMLImageElement>) => void;
|
||||
disableTooltipAndBadge?: boolean;
|
||||
imageFit?: "cover" | "contain";
|
||||
onDeleted?: (id: string) => void;
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface GalleryItem {
|
||||
id: string;
|
||||
label: string;
|
||||
thumbnail: string | null;
|
||||
// Thumbnail template is not a usable URL yet. It has some variables
|
||||
// Thumbnail template is not a usable URL yet. It has some variables
|
||||
// like `{WIDTH}` which must be replaced downstream.
|
||||
thumbnailUrlTemplate?: string;
|
||||
fullImage?: string | null;
|
||||
@@ -77,6 +77,10 @@ interface GalleryModalProps {
|
||||
isOpen?: boolean;
|
||||
forceFilter?: string;
|
||||
onEditClicked?: (url: string, media_id?: string) => Promise<void> | void;
|
||||
onTurnIntoVideoClicked?: (
|
||||
url: string,
|
||||
media_id?: string
|
||||
) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const GalleryModal = React.memo(
|
||||
@@ -92,6 +96,7 @@ export const GalleryModal = React.memo(
|
||||
isOpen,
|
||||
forceFilter,
|
||||
onEditClicked,
|
||||
onTurnIntoVideoClicked,
|
||||
}: GalleryModalProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
// Lightbox state is now handled via signals
|
||||
@@ -126,6 +131,8 @@ export const GalleryModal = React.memo(
|
||||
const pageSize = 100;
|
||||
|
||||
const imageUrl = lightboxImageSignal.value?.fullImage || "";
|
||||
const imageUrls: string[] | undefined = (lightboxImageSignal.value as any)
|
||||
?.imageUrls;
|
||||
|
||||
const api = useMemo(() => new GalleryModalApi(), []);
|
||||
const usersApi = useMemo(() => new UsersApi(), []);
|
||||
@@ -269,9 +276,9 @@ export const GalleryModal = React.memo(
|
||||
"{WIDTH}",
|
||||
thumbnail_size.toString()
|
||||
),
|
||||
// TODO(bt): Thumbnail template URL may be wrong for videos and other asset types, but I'm
|
||||
// TODO(bt): Thumbnail template URL may be wrong for videos and other asset types, but I'm
|
||||
// trying to plumb it through for the inpainting editor, which is just images.
|
||||
thumbnailUrlTemplate: item.media_links.maybe_thumbnail_template,
|
||||
thumbnailUrlTemplate: item.media_links.maybe_thumbnail_template,
|
||||
fullImage: item.media_links.cdn_url,
|
||||
createdAt: item.created_at,
|
||||
mediaClass:
|
||||
@@ -630,9 +637,11 @@ export const GalleryModal = React.memo(
|
||||
activeFilter={activeFilter}
|
||||
selected={selectedItemIds.includes(item.id)}
|
||||
onClick={() => handleItemClick(item)}
|
||||
onImageError={() =>
|
||||
handleImageError(item.thumbnail!)
|
||||
}
|
||||
onImageError={(e) => {
|
||||
// NB: Replace the broken thumbnail with a placeholder.
|
||||
e.currentTarget.src = "/resources/placeholders/placeholder.png";
|
||||
handleImageError(item.thumbnail!);
|
||||
}}
|
||||
disableTooltipAndBadge={mode === "select"}
|
||||
imageFit={imageFit}
|
||||
onDeleted={handleItemDeleted}
|
||||
@@ -693,16 +702,20 @@ export const GalleryModal = React.memo(
|
||||
onClose={handleCloseLightbox}
|
||||
onCloseGallery={() => (galleryModalVisibleViewMode.value = false)}
|
||||
imageUrl={imageUrl}
|
||||
// pass multiple images if present
|
||||
imageUrls={imageUrls}
|
||||
imageAlt={lightboxImageSignal.value?.label || ""}
|
||||
onImageError={() => imageUrl && handleImageError(imageUrl)}
|
||||
title={lightboxImageSignal.value?.label}
|
||||
createdAt={lightboxImageSignal.value?.createdAt}
|
||||
downloadUrl={imageUrl}
|
||||
mediaId={lightboxImageSignal.value?.id}
|
||||
mediaTokens={(lightboxImageSignal.value as any)?.mediaTokens}
|
||||
onDownloadClicked={onDownloadClicked}
|
||||
onAddToSceneClicked={onAddToSceneClicked}
|
||||
mediaClass={lightboxImageSignal.value?.mediaClass}
|
||||
onEditClicked={onEditClicked}
|
||||
onTurnIntoVideoClicked={onTurnIntoVideoClicked}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import dayjs from "dayjs";
|
||||
import { faDownToLine, faPencil } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faCube,
|
||||
faDownToLine,
|
||||
faPencil,
|
||||
faVideo,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
EnqueueImageTo3dObject,
|
||||
EnqueueImageTo3dObjectModel,
|
||||
} from "@storyteller/tauri-api";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import React from "react";
|
||||
import { useEffect, useState, ReactNode } from "react";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { MediaFilesApi, PromptsApi } from "@storyteller/api";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
@@ -19,18 +24,19 @@ import {
|
||||
getModelDisplayName,
|
||||
getProviderDisplayName,
|
||||
} from "@storyteller/model-list";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
|
||||
interface LightboxModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCloseGallery: () => void;
|
||||
imageUrl?: string | null;
|
||||
imageUrls?: string[];
|
||||
mediaTokens?: string[];
|
||||
imageAlt?: string;
|
||||
onImageError?: () => void;
|
||||
title?: string;
|
||||
createdAt?: string;
|
||||
additionalInfo?: React.ReactNode;
|
||||
additionalInfo?: ReactNode;
|
||||
downloadUrl?: string;
|
||||
mediaId?: string;
|
||||
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
|
||||
@@ -41,6 +47,10 @@ interface LightboxModalProps {
|
||||
mediaClass?: string;
|
||||
onPromptCopy?: (prompt: string) => void;
|
||||
onEditClicked?: (url: string, media_id?: string) => Promise<void> | void;
|
||||
onTurnIntoVideoClicked?: (
|
||||
url: string,
|
||||
media_id?: string
|
||||
) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function LightboxModal({
|
||||
@@ -48,6 +58,8 @@ export function LightboxModal({
|
||||
onClose,
|
||||
onCloseGallery,
|
||||
imageUrl,
|
||||
imageUrls,
|
||||
mediaTokens,
|
||||
imageAlt = "",
|
||||
onImageError,
|
||||
title,
|
||||
@@ -59,23 +71,32 @@ export function LightboxModal({
|
||||
onAddToSceneClicked,
|
||||
mediaClass,
|
||||
onEditClicked,
|
||||
onTurnIntoVideoClicked,
|
||||
}: LightboxModalProps) {
|
||||
// NB(bt,2025-06-14): We add ?cors=1 to the image url to prevent caching "sec-fetch-mode: no-cors" from
|
||||
// the <image> tag request from being cached. If we then drag it into the canvas after it's been cached,
|
||||
// it won't be able to load in cors mode and will show blank in the canvas and 3D engine. This is a really
|
||||
// stupid hack around this behavior.
|
||||
const imageTagImageUrl = imageUrl ? imageUrl + "?cors=1" : "";
|
||||
const [selectedBatchIndex, setSelectedBatchIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const displayUrl =
|
||||
selectedBatchIndex !== null && imageUrls
|
||||
? imageUrls[selectedBatchIndex]
|
||||
: imageUrl || null;
|
||||
const [refPreviewUrl, setRefPreviewUrl] = useState<string | null>(null);
|
||||
const imageTagImageUrl = displayUrl ? displayUrl + "?cors=1" : "";
|
||||
|
||||
const [mediaLoaded, setMediaLoaded] = React.useState<boolean>(false);
|
||||
const [prompt, setPrompt] = React.useState<string | null>(null);
|
||||
const [promptLoading, setPromptLoading] = React.useState<boolean>(false);
|
||||
const [hasPromptToken, setHasPromptToken] = React.useState<boolean>(false);
|
||||
const [isPromptHovered, setIsPromptHovered] = React.useState<boolean>(false);
|
||||
const [generationProvider, setGenerationProvider] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [modelType, setModelType] = React.useState<string | null>(null);
|
||||
const [contextImages, setContextImages] = React.useState<Array<{
|
||||
const [mediaLoaded, setMediaLoaded] = useState<boolean>(false);
|
||||
const [prompt, setPrompt] = useState<string | null>(null);
|
||||
const [promptLoading, setPromptLoading] = useState<boolean>(false);
|
||||
const [hasPromptToken, setHasPromptToken] = useState<boolean>(false);
|
||||
const [isPromptHovered, setIsPromptHovered] = useState<boolean>(false);
|
||||
const [generationProvider, setGenerationProvider] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [modelType, setModelType] = useState<string | null>(null);
|
||||
const [contextImages, setContextImages] = useState<Array<{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
@@ -85,14 +106,35 @@ export function LightboxModal({
|
||||
}> | null>(null);
|
||||
|
||||
// Reset when imageUrl changes
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setMediaLoaded(false);
|
||||
}, [imageUrl]);
|
||||
}, [imageUrl, selectedBatchIndex]);
|
||||
|
||||
// Maintain current media id (updates when selecting from batch)
|
||||
const [currentMediaId, setCurrentMediaId] = useState<string | undefined>(
|
||||
mediaId
|
||||
);
|
||||
useEffect(() => {
|
||||
setCurrentMediaId(mediaId);
|
||||
}, [mediaId]);
|
||||
|
||||
// Reset promoted selection whenever a new lightbox item is shown or dialog re-opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedBatchIndex(null);
|
||||
setRefPreviewUrl(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
useEffect(() => {
|
||||
// If the upstream lightbox content changes, return to grid state
|
||||
setSelectedBatchIndex(null);
|
||||
setRefPreviewUrl(null);
|
||||
}, [mediaId, imageUrls]);
|
||||
|
||||
// Fetch prompt when mediaId changes
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const fetchPrompt = async () => {
|
||||
if (!mediaId) {
|
||||
if (!currentMediaId) {
|
||||
setPrompt(null);
|
||||
setHasPromptToken(false);
|
||||
setGenerationProvider(null);
|
||||
@@ -105,7 +147,7 @@ export function LightboxModal({
|
||||
try {
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const mediaResponse = await mediaFilesApi.GetMediaFileByToken({
|
||||
mediaFileToken: mediaId,
|
||||
mediaFileToken: currentMediaId,
|
||||
});
|
||||
|
||||
if (mediaResponse.success && mediaResponse.data?.maybe_prompt_token) {
|
||||
@@ -147,333 +189,394 @@ export function LightboxModal({
|
||||
};
|
||||
|
||||
fetchPrompt();
|
||||
}, [mediaId]);
|
||||
}, [currentMediaId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="rounded-xl bg-[#2C2C2C] h-[75vh] w-[60vw] max-w-screen min-w-[35vw] min-h-[40vh] p-4"
|
||||
draggable
|
||||
allowBackgroundInteraction={true}
|
||||
showClose={true}
|
||||
closeOnOutsideClick={false}
|
||||
resizable={true}
|
||||
backdropClassName="pointer-events-none hidden"
|
||||
expandable={true}
|
||||
>
|
||||
{/* Invisible drag handle strip at the very top for moving */}
|
||||
<Modal.DragHandle>
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" />
|
||||
</Modal.DragHandle>
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="rounded-xl bg-ui-modal h-[75vh] w-[60vw] max-w-screen min-w-[35vw] min-h-[40vh] p-4"
|
||||
draggable
|
||||
allowBackgroundInteraction={true}
|
||||
showClose={true}
|
||||
closeOnOutsideClick={false}
|
||||
resizable={true}
|
||||
backdropClassName="pointer-events-none hidden"
|
||||
expandable={true}
|
||||
>
|
||||
{/* Invisible drag handle strip at the very top for moving */}
|
||||
<Modal.DragHandle>
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" />
|
||||
</Modal.DragHandle>
|
||||
|
||||
{/* content grid */}
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
{/* image panel */}
|
||||
<div className="col-span-2 relative flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-[#1A1A1A]">
|
||||
{!imageUrl ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-800">
|
||||
<span className="text-white/60">Image not available</span>
|
||||
</div>
|
||||
) : mediaClass === "video" ? (
|
||||
<video
|
||||
controls
|
||||
loop={true}
|
||||
autoPlay={true}
|
||||
className="h-full w-full object-contain"
|
||||
onLoadedData={() => setMediaLoaded(true)}
|
||||
>
|
||||
<source src={imageUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
) : (
|
||||
<img
|
||||
data-lightbox-modal="true"
|
||||
src={imageTagImageUrl}
|
||||
alt={imageAlt}
|
||||
className="h-full w-full object-contain"
|
||||
onError={onImageError}
|
||||
onLoad={() => setMediaLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!mediaLoaded && imageUrl && (
|
||||
<div className="absolute inset-0 bg-[#1A1A1A] flex items-center justify-center">
|
||||
<LoadingSpinner className="h-12 w-12 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* info + actions */}
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-1 space-y-5">
|
||||
{/* <div className="text-xl font-medium">
|
||||
{title || "Image Generation"}
|
||||
</div> */}
|
||||
{createdAt && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">Created</div>
|
||||
<div className="text-sm text-white/70">
|
||||
{dayjs(createdAt).format("MMM D, YYYY")} at{" "}
|
||||
{dayjs(createdAt).format("hh:mm A")}
|
||||
</div>
|
||||
{/* content grid */}
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
{/* image panel */}
|
||||
<div className="col-span-2 relative flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-black/30">
|
||||
{!displayUrl ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-black/30">
|
||||
<span className="text-base-fg/60">Image not available</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasPromptToken && (
|
||||
<>
|
||||
{/* Prompt */}
|
||||
<div className="relative space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">
|
||||
Prompt
|
||||
</div>
|
||||
) : mediaClass === "video" ? (
|
||||
<video
|
||||
controls
|
||||
loop={true}
|
||||
autoPlay={true}
|
||||
className="h-full w-full object-contain"
|
||||
onLoadedData={() => setMediaLoaded(true)}
|
||||
>
|
||||
<source src={displayUrl as string} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
) : imageUrls &&
|
||||
imageUrls.length > 1 &&
|
||||
selectedBatchIndex === null ? (
|
||||
<div
|
||||
className="grid w-full h-full p-2 gap-2"
|
||||
style={{ gridTemplateColumns: "repeat(2, minmax(0, 1fr))" }}
|
||||
>
|
||||
{imageUrls.slice(0, 4).map((url, idx) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative text-sm text-white/90 break-words bg-black/20 p-3 rounded-lg cursor-pointer transition-colors duration-100 leading-relaxed",
|
||||
isPromptHovered && "bg-black/30"
|
||||
)}
|
||||
onMouseEnter={() => setIsPromptHovered(true)}
|
||||
onMouseLeave={() => setIsPromptHovered(false)}
|
||||
key={idx}
|
||||
className="relative flex items-center justify-center overflow-hidden rounded-lg bg-black/20 cursor-pointer"
|
||||
onClick={() => {
|
||||
if (!prompt) return;
|
||||
navigator.clipboard.writeText(prompt).catch(() => {});
|
||||
toast.success("Prompt copied");
|
||||
setSelectedBatchIndex(idx);
|
||||
const maybeToken = mediaTokens?.[idx];
|
||||
if (maybeToken) setCurrentMediaId(maybeToken);
|
||||
}}
|
||||
>
|
||||
{promptLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<LoadingSpinner className="h-4 w-4" />
|
||||
<span className="text-sm text-white/80">
|
||||
Loading prompt...
|
||||
</span>
|
||||
<img
|
||||
src={url + "?cors=1"}
|
||||
alt={`Generated ${idx + 1}`}
|
||||
className="h-full w-full object-contain"
|
||||
onLoad={() => setMediaLoaded(true)}
|
||||
onError={onImageError}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
data-lightbox-modal="true"
|
||||
src={imageTagImageUrl}
|
||||
alt={imageAlt}
|
||||
className="h-full w-full object-contain"
|
||||
onError={onImageError}
|
||||
onLoad={() => setMediaLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!mediaLoaded && displayUrl && (
|
||||
<div className="absolute inset-0 bg-ui-panel flex items-center justify-center">
|
||||
<LoadingSpinner className="h-12 w-12 text-base-fg" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* info + actions */}
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-1 space-y-5 text-base-fg">
|
||||
{/* <div className="text-xl font-medium">
|
||||
{title || "Image Generation"}
|
||||
</div> */}
|
||||
{createdAt && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-base-fg/90">
|
||||
Created
|
||||
</div>
|
||||
<div className="text-sm text-base-fg/70">
|
||||
{dayjs(createdAt).format("MMM D, YYYY")} at{" "}
|
||||
{dayjs(createdAt).format("hh:mm A")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasPromptToken && (
|
||||
<>
|
||||
{/* Prompt */}
|
||||
<div className="relative space-y-1.5">
|
||||
<div className="text-sm font-medium text-base-fg/90">
|
||||
Prompt
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative text-sm text-base-fg break-words p-3 rounded-lg cursor-pointer transition-colors duration-100 leading-relaxed"
|
||||
)}
|
||||
style={{
|
||||
background: isPromptHovered
|
||||
? "rgb(var(--st-controls-rgb) / 0.30)"
|
||||
: "rgb(var(--st-controls-rgb) / 0.20)",
|
||||
}}
|
||||
onMouseEnter={() => setIsPromptHovered(true)}
|
||||
onMouseLeave={() => setIsPromptHovered(false)}
|
||||
onClick={() => {
|
||||
if (!prompt) return;
|
||||
navigator.clipboard.writeText(prompt).catch(() => {});
|
||||
toast.success("Prompt copied");
|
||||
}}
|
||||
>
|
||||
{promptLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<LoadingSpinner className="h-4 w-4" />
|
||||
<span className="text-sm text-base-fg/80">
|
||||
Loading prompt...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
prompt || (
|
||||
<span className="text-sm text-base-fg">
|
||||
No prompt
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!promptLoading && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"pointer-events-none absolute inset-0 flex items-end justify-end opacity-0 transition-opacity duration-50",
|
||||
isPromptHovered && "opacity-100"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-1 text-xs text-base-fg backdrop-blur-md p-1.5 rounded-tl-lg rounded-br-lg"
|
||||
style={{
|
||||
background: "rgb(var(--st-controls-rgb) / 0.80)",
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} className="h-3 w-3" />
|
||||
<span>Copy prompt</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
prompt || (
|
||||
<span className="text-sm text-white/90">No prompt</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!promptLoading && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"pointer-events-none absolute inset-0 flex items-end justify-end opacity-0 transition-opacity duration-50",
|
||||
isPromptHovered && "opacity-100"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs text-white/80 bg-black/80 backdrop-blur-md p-1.5 rounded-tl-lg rounded-br-lg">
|
||||
<FontAwesomeIcon icon={faCopy} className="h-3 w-3" />
|
||||
<span>Copy prompt</span>
|
||||
{/* Context Images */}
|
||||
{contextImages && contextImages.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-base-fg/90">
|
||||
Reference Images
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{contextImages.map((contextImage, index) => {
|
||||
const thumbnailUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"128"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
const fullSizeUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"512"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={contextImage.media_token}
|
||||
className="glass relative aspect-square overflow-hidden rounded-lg w-14 border-2 border-white/30 hover:border-white/80 transition-all group cursor-pointer hover:cursor-zoom-in"
|
||||
onClick={() => setRefPreviewUrl(fullSizeUrl)}
|
||||
>
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={`Reference image ${index + 1}`}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context Images */}
|
||||
{contextImages && contextImages.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">
|
||||
Reference Images
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{contextImages.map((contextImage, index) => {
|
||||
const thumbnailUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"128"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
const fullSizeUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"512"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
return (
|
||||
// Hover preview
|
||||
<Tooltip
|
||||
key={contextImage.media_token}
|
||||
className="bg-black p-1.5"
|
||||
content={
|
||||
<div>
|
||||
<div className="flex flex-col items-center bg-white/10 rounded-lg">
|
||||
<img
|
||||
src={fullSizeUrl}
|
||||
alt={`Reference image ${index + 1} preview`}
|
||||
className="w-auto h-48 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
{contextImage.semantic && (
|
||||
<div className="mt-2 text-xs text-white/90 text-center max-w-48 px-1">
|
||||
{contextImage.semantic}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
position="top"
|
||||
delay={300}
|
||||
closeOnClick={true}
|
||||
{/* Generation Details */}
|
||||
{(generationProvider || modelType) && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-base-fg/90">
|
||||
Generation Details
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{modelType && (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-3 rounded-lg border border-ui-panel-border"
|
||||
style={{
|
||||
background: "rgb(var(--st-controls-rgb) / 0.20)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative group cursor-pointer"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
contextImage.media_links.cdn_url,
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-lg border border-white/5 bg-white/10 aspect-square">
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={`Reference image ${index + 1}`}
|
||||
className="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-base-fg/70 font-medium">
|
||||
Model
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{getModelCreatorIcon(modelType)}
|
||||
<span className="text-sm text-base-fg rounded">
|
||||
{getModelDisplayName(modelType)}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generation Details */}
|
||||
{(generationProvider || modelType) && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">
|
||||
Generation Details
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{modelType && (
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-black/20 rounded-lg border border-white/5">
|
||||
<span className="text-sm text-white/70 font-medium">
|
||||
Model
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{getModelCreatorIcon(modelType)}
|
||||
<span className="text-sm text-white/90 rounded">
|
||||
{getModelDisplayName(modelType)}
|
||||
</div>
|
||||
)}
|
||||
{generationProvider && (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-3 rounded-lg border border-ui-panel-border"
|
||||
style={{
|
||||
background: "rgb(var(--st-controls-rgb) / 0.20)",
|
||||
}}
|
||||
>
|
||||
<span className="text-sm text-base-fg/70 font-medium">
|
||||
Provider
|
||||
</span>
|
||||
<span className="text-sm text-base-fg rounded">
|
||||
{getProviderDisplayName(generationProvider)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{additionalInfo}
|
||||
</div>
|
||||
|
||||
{/* buttons with spacing */}
|
||||
{(onAddToSceneClicked && downloadUrl) || downloadUrl
|
||||
? (() => {
|
||||
const visibleButtons = [
|
||||
onEditClicked && downloadUrl && mediaClass === "image",
|
||||
onTurnIntoVideoClicked &&
|
||||
downloadUrl &&
|
||||
mediaClass === "image",
|
||||
onAddToSceneClicked && downloadUrl,
|
||||
mediaClass === "image",
|
||||
onDownloadClicked && downloadUrl,
|
||||
].filter(Boolean).length;
|
||||
|
||||
const buttonClass =
|
||||
visibleButtons === 1 ? "w-full col-span-2" : "w-full";
|
||||
|
||||
return (
|
||||
<div className="mt-15 mb-15 grid grid-cols-2 gap-2">
|
||||
{onEditClicked &&
|
||||
downloadUrl &&
|
||||
mediaClass === "image" && (
|
||||
<Button
|
||||
className={buttonClass}
|
||||
icon={faPencil}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("edit_image_clicked");
|
||||
await onEditClicked(downloadUrl, mediaId);
|
||||
}}
|
||||
>
|
||||
Edit Image
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onTurnIntoVideoClicked &&
|
||||
downloadUrl &&
|
||||
mediaClass === "image" && (
|
||||
<Button
|
||||
className={buttonClass}
|
||||
icon={faVideo}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("turn_into_video_clicked");
|
||||
await onTurnIntoVideoClicked(
|
||||
downloadUrl,
|
||||
mediaId
|
||||
);
|
||||
}}
|
||||
>
|
||||
Turn into Video
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onAddToSceneClicked && downloadUrl && (
|
||||
<Button
|
||||
className={buttonClass}
|
||||
variant="secondary"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("add_to_scene_clicked");
|
||||
await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
onClose();
|
||||
onCloseGallery();
|
||||
}}
|
||||
>
|
||||
Add to Current Scene
|
||||
</Button>
|
||||
)}
|
||||
{generationProvider && (
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-black/20 rounded-lg border border-white/5">
|
||||
<span className="text-sm text-white/70 font-medium">
|
||||
Provider
|
||||
</span>
|
||||
<span className="text-sm text-white/90 rounded">
|
||||
{getProviderDisplayName(generationProvider)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{mediaClass === "image" && (
|
||||
<Button
|
||||
icon={faCube}
|
||||
className={buttonClass}
|
||||
variant="secondary"
|
||||
onClick={async (e) => {
|
||||
gtagEvent("image_to_3d_clicked");
|
||||
await EnqueueImageTo3dObject({
|
||||
image_media_token: mediaId,
|
||||
model: EnqueueImageTo3dObjectModel.Hunyuan3d2_0,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Make 3D Model
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onDownloadClicked && downloadUrl && (
|
||||
<Button
|
||||
className={buttonClass}
|
||||
icon={faDownToLine}
|
||||
variant="secondary"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("download_clicked");
|
||||
await onDownloadClicked(downloadUrl, mediaClass);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{additionalInfo}
|
||||
);
|
||||
})()
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{/* buttons with spacing */}
|
||||
{(onAddToSceneClicked && downloadUrl) || downloadUrl ? (
|
||||
<div className="mt-15 mb-15 flex justify-end gap-2">
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
gtagEvent("image_to_3d_clicked");
|
||||
//let _result = await FalHunyuanImageTo3d({
|
||||
// image_media_token: mediaId,
|
||||
// //base64_image: downloadUrl,
|
||||
//});
|
||||
let result = await EnqueueImageTo3dObject({
|
||||
image_media_token: mediaId,
|
||||
model: EnqueueImageTo3dObjectModel.Hunyuan3d2_0,
|
||||
});
|
||||
//e.stopPropagation();
|
||||
//await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
//onClose(); // close the lightbox
|
||||
//onCloseGallery(); // close the gallery
|
||||
}}
|
||||
>
|
||||
3D
|
||||
</Button>
|
||||
|
||||
{onEditClicked && downloadUrl && (
|
||||
<Button
|
||||
icon={faPencil}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("edit_image_clicked");
|
||||
await onEditClicked(downloadUrl, mediaId);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onAddToSceneClicked && downloadUrl && (
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("add_to_scene_clicked");
|
||||
await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
onClose(); // close the lightbox
|
||||
onCloseGallery(); // close the gallery
|
||||
}}
|
||||
>
|
||||
Add to Current Scene
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* {downloadUrl &&
|
||||
(onDownloadClicked ? (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("download_clicked");
|
||||
await onDownloadClicked(downloadUrl, mediaClass);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
) : (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("download_clicked");
|
||||
}}
|
||||
className="no-underline"
|
||||
>
|
||||
<Button icon={faDownToLine}>Download</Button>
|
||||
</a>
|
||||
))} */}
|
||||
|
||||
{onDownloadClicked && downloadUrl && (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("download_clicked");
|
||||
await onDownloadClicked(downloadUrl, mediaClass);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal>
|
||||
|
||||
{refPreviewUrl && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => setRefPreviewUrl(null)}
|
||||
className="rounded-xl bg-ui-modal h-[50vh] w-fit max-w-screen min-w-[35vw] min-h-[40vh] p-4"
|
||||
draggable
|
||||
allowBackgroundInteraction={true}
|
||||
showClose={true}
|
||||
closeOnOutsideClick={true}
|
||||
resizable={true}
|
||||
backdropClassName=""
|
||||
expandable={true}
|
||||
>
|
||||
<Modal.DragHandle>
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" />
|
||||
</Modal.DragHandle>
|
||||
<div className="relative flex h-full items-center justify-center overflow-hidden rounded-xl bg-black/30">
|
||||
<img
|
||||
src={`${refPreviewUrl}?cors=1`}
|
||||
alt="Reference preview"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ export const PopoverMenu = ({
|
||||
const className = twMerge(
|
||||
"text-sm font-medium rounded-lg px-3 py-2 shadow-sm",
|
||||
"flex gap-2 items-center justify-center outline-none",
|
||||
"transition-all duration-100",
|
||||
"bg-ui-controls/60 px-3 text-base-fg hover:bg-ui-controls/90 border border-ui-controls-border",
|
||||
"transition-all duration-150",
|
||||
"bg-ui-controls px-3 text-base-fg hover:bg-ui-controls/80 border border-ui-controls-border",
|
||||
"active:scale-95 transform",
|
||||
buttonClassName
|
||||
);
|
||||
|
||||
@@ -6,7 +6,10 @@ import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { Button, ToggleButton } from "@storyteller/ui-button";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { EnqueueImageToVideo, EnqueueImageToVideoRequest } from "@storyteller/tauri-api";
|
||||
import {
|
||||
EnqueueImageToVideo,
|
||||
EnqueueImageToVideoRequest,
|
||||
} from "@storyteller/tauri-api";
|
||||
import {
|
||||
faMessageXmark,
|
||||
faMessageCheck,
|
||||
@@ -14,12 +17,17 @@ import {
|
||||
faSpinnerThird,
|
||||
faSquare,
|
||||
faPortrait,
|
||||
faWaveformLines,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faRectangle } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { SizeIconOption, SizeOption, VideoModel } from "@storyteller/model-list";
|
||||
import {
|
||||
SizeIconOption,
|
||||
SizeOption,
|
||||
VideoModel,
|
||||
} from "@storyteller/model-list";
|
||||
import { usePromptVideoStore, RefImage } from "./promptStore";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { ImagePromptRow } from "./ImagePromptRow";
|
||||
@@ -27,7 +35,7 @@ import type { UploadImageFn } from "./ImagePromptRow";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
|
||||
const DEFAULT_RESOLUTIONS : SizeOption[] = [
|
||||
const DEFAULT_RESOLUTIONS: SizeOption[] = [
|
||||
{
|
||||
tauriValue: "720p",
|
||||
textLabel: "720p",
|
||||
@@ -83,6 +91,10 @@ export const PromptBoxVideo = ({
|
||||
const setPrompt = usePromptVideoStore((s) => s.setPrompt);
|
||||
const useSystemPrompt = usePromptVideoStore((s) => s.useSystemPrompt);
|
||||
const setUseSystemPrompt = usePromptVideoStore((s) => s.setUseSystemPrompt);
|
||||
const generateWithSound = usePromptVideoStore((s) => s.generateWithSound);
|
||||
const setGenerateWithSound = usePromptVideoStore(
|
||||
(s) => s.setGenerateWithSound
|
||||
);
|
||||
const resolution = usePromptVideoStore((s) => s.resolution);
|
||||
const setResolution = usePromptVideoStore((s) => s.setResolution);
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
@@ -109,7 +121,9 @@ export const PromptBoxVideo = ({
|
||||
if (!!selectedModel?.sizeOptions && selectedModel.sizeOptions.length > 0) {
|
||||
// When switching to a new model, the existing resolution might not be correct.
|
||||
// This is a gross and nasty hack to handle the case where the resolution is not found.
|
||||
const resolutionExists = selectedModel.sizeOptions.some((option) => option.textLabel === resolution);
|
||||
const resolutionExists = selectedModel.sizeOptions.some(
|
||||
(option) => option.textLabel === resolution
|
||||
);
|
||||
const useFirstOption = !resolutionExists;
|
||||
|
||||
resolutionOptions = selectedModel.sizeOptions.map((option, index) => {
|
||||
@@ -128,12 +142,15 @@ export const PromptBoxVideo = ({
|
||||
const icon = <FontAwesomeIcon icon={faIcon} className="h-4 w-4" />;
|
||||
return {
|
||||
label: option.textLabel,
|
||||
selected: option.textLabel === resolution || (useFirstOption && index === 0),
|
||||
selected:
|
||||
option.textLabel === resolution || (useFirstOption && index === 0),
|
||||
icon: icon,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const resolutionExists = DEFAULT_RESOLUTIONS.some((option) => option.textLabel === resolution);
|
||||
const resolutionExists = DEFAULT_RESOLUTIONS.some(
|
||||
(option) => option.textLabel === resolution
|
||||
);
|
||||
const useFirstOption = !resolutionExists;
|
||||
|
||||
resolutionOptions = DEFAULT_RESOLUTIONS.map((option, index) => {
|
||||
@@ -152,13 +169,15 @@ export const PromptBoxVideo = ({
|
||||
const icon = <FontAwesomeIcon icon={faIcon} className="h-4 w-4" />;
|
||||
return {
|
||||
label: option.textLabel,
|
||||
selected: option.textLabel === resolution || (useFirstOption && index === 0),
|
||||
selected:
|
||||
option.textLabel === resolution || (useFirstOption && index === 0),
|
||||
icon: icon,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const [resolutionList, setResolutionList] = useState<PopoverItem[]>(resolutionOptions);
|
||||
const [resolutionList, setResolutionList] =
|
||||
useState<PopoverItem[]>(resolutionOptions);
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -252,7 +271,7 @@ export const PromptBoxVideo = ({
|
||||
setIsEnqueueing(false);
|
||||
}, 10000);
|
||||
|
||||
let request : EnqueueImageToVideoRequest = {
|
||||
let request: EnqueueImageToVideoRequest = {
|
||||
model: selectedModel,
|
||||
image_media_token: imageMediaToken,
|
||||
prompt: prompt,
|
||||
@@ -262,7 +281,16 @@ export const PromptBoxVideo = ({
|
||||
};
|
||||
|
||||
if (selectedModel?.tauriId === "sora_2") {
|
||||
request.sora_orientation = resolution === "720p" ? "landscape" : "portrait";
|
||||
request.sora_orientation =
|
||||
resolution === "720p" ? "landscape" : "portrait";
|
||||
}
|
||||
|
||||
if (selectedModel?.tauriId === "veo_2") {
|
||||
request.generate_with_sound = !!generateWithSound;
|
||||
}
|
||||
|
||||
if (selectedModel?.tauriId === "veo_3") {
|
||||
request.generate_with_sound = !!generateWithSound;
|
||||
}
|
||||
|
||||
await EnqueueImageToVideo(request);
|
||||
@@ -297,9 +325,8 @@ export const PromptBoxVideo = ({
|
||||
|
||||
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
|
||||
|
||||
const modelNeedsAnImageButNoneAreSelected =
|
||||
selectedModel?.requiresImage &&
|
||||
referenceImages.length === 0;
|
||||
const modelNeedsAnImageButNoneAreSelected =
|
||||
selectedModel?.requiresImage && referenceImages.length === 0;
|
||||
|
||||
// Hide/clear ending frame if model doesn't support it
|
||||
useEffect(() => {
|
||||
@@ -421,6 +448,7 @@ export const PromptBoxVideo = ({
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
content={
|
||||
useSystemPrompt
|
||||
@@ -438,6 +466,22 @@ export const PromptBoxVideo = ({
|
||||
onClick={() => setUseSystemPrompt(!useSystemPrompt)}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{selectedModel?.generateWithSound && (
|
||||
<Tooltip
|
||||
content={generateWithSound ? "Sound: ON" : "Sound: OFF"}
|
||||
position="top"
|
||||
className="z-50"
|
||||
delay={200}
|
||||
>
|
||||
<ToggleButton
|
||||
isActive={generateWithSound}
|
||||
icon={faWaveformLines}
|
||||
activeIcon={faWaveformLines}
|
||||
onClick={() => setGenerateWithSound(!generateWithSound)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
|
||||
@@ -93,11 +93,13 @@ interface PromptVideoStore {
|
||||
useSystemPrompt: boolean;
|
||||
referenceImages: RefImage[];
|
||||
endFrameImage?: RefImage;
|
||||
generateWithSound: boolean;
|
||||
setPrompt: (prompt: string) => void;
|
||||
setResolution: (resolution: Resolution | string) => void;
|
||||
setUseSystemPrompt: (value: boolean) => void;
|
||||
setReferenceImages: (images: RefImage[]) => void;
|
||||
setEndFrameImage: (image?: RefImage) => void;
|
||||
setGenerateWithSound: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
|
||||
@@ -106,11 +108,13 @@ export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
|
||||
useSystemPrompt: true,
|
||||
referenceImages: [],
|
||||
endFrameImage: undefined,
|
||||
generateWithSound: true,
|
||||
setPrompt: (prompt) => set({ prompt }),
|
||||
setResolution: (resolution) => set({ resolution }),
|
||||
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
|
||||
setReferenceImages: (referenceImages) => set({ referenceImages }),
|
||||
setEndFrameImage: (endFrameImage) => set({ endFrameImage }),
|
||||
setGenerateWithSound: (generateWithSound) => set({ generateWithSound }),
|
||||
}));
|
||||
|
||||
// ----- Edit Prompt Box Store -----
|
||||
|
||||
@@ -48,6 +48,7 @@ export class ImageModel extends Model {
|
||||
maxImagePromptCount?: number;
|
||||
canTextToImage?: boolean;
|
||||
tags?: ModelTag[];
|
||||
progressBarTime?: number;
|
||||
}) {
|
||||
if (args.maxGenerationCount < 1) {
|
||||
throw new Error("maxGenerationCount must be at least 1");
|
||||
|
||||
@@ -43,6 +43,9 @@ export class Model {
|
||||
// Optional provider preferences; two-to-many via page and providers.
|
||||
readonly preferredProvidersByPage?: Partial<Record<string, string[]>>;
|
||||
|
||||
// Time in milliseconds for the fake progress bar to reach 100% (UI-only)
|
||||
readonly progressBarTime: number;
|
||||
|
||||
protected constructor(args: {
|
||||
id: string;
|
||||
tauriId: string;
|
||||
@@ -54,6 +57,7 @@ export class Model {
|
||||
selectorBadges: string[];
|
||||
tags?: ModelTag[];
|
||||
preferredProvidersByPage?: Partial<Record<string, string[]>>;
|
||||
progressBarTime?: number;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.tauriId = args.tauriId;
|
||||
@@ -65,6 +69,7 @@ export class Model {
|
||||
this.selectorBadges = args.selectorBadges;
|
||||
this.tags = args.tags ?? [];
|
||||
this.preferredProvidersByPage = args.preferredProvidersByPage;
|
||||
this.progressBarTime = args.progressBarTime ?? 20000;
|
||||
}
|
||||
|
||||
toLegacyBadges(): { label: string }[] {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user