Compare commits

...
Author SHA1 Message Date
Brandon Thomas 7b12091eb1 refactor out into function 2025-08-22 00:54:21 -04:00
Brandon Thomas 5b62a439ec send text-to-image events 2025-08-21 23:45:32 -04:00
27 changed files with 455 additions and 173 deletions
@@ -31,6 +31,7 @@ use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
@@ -45,6 +46,21 @@ pub struct EnqueueTextToImageRequest {
/// The number of images to generate.
pub number_images: Option<u32>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
// TODO(bt,2025-07-14): Support other aspect ratios / resolutions -
@@ -186,7 +202,7 @@ pub async fn handle_request(
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let result = dispatch_request(
request,
&request,
&app,
&app_data_root,
&provider_priority_store,
@@ -205,7 +221,12 @@ pub async fn handle_request(
};
let result = success_event
.insert_into_task_database(task_database)
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller,
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref(),
)
.await;
if let Err(err) = result {
@@ -217,7 +238,7 @@ pub async fn handle_request(
}
pub async fn dispatch_request(
request: EnqueueTextToImageRequest,
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
@@ -25,7 +25,7 @@ use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_imag
use tauri::AppHandle;
pub async fn handle_image_artcraft(
request: EnqueueTextToImageRequest,
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
@@ -71,7 +71,7 @@ pub async fn handle_image_artcraft(
selected_model = GenerationModel::Flux1Dev;
let request = GenerateFlux1DevTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
prompt: request.prompt.clone(),
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
@@ -110,7 +110,7 @@ pub async fn handle_image_artcraft(
selected_model = GenerationModel::Flux1Schnell;
let request = GenerateFlux1SchnellTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
prompt: request.prompt.clone(),
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
@@ -149,7 +149,7 @@ pub async fn handle_image_artcraft(
selected_model = GenerationModel::FluxPro11;
let request = GenerateFluxPro11TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
prompt: request.prompt.clone(),
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
@@ -188,7 +188,7 @@ pub async fn handle_image_artcraft(
selected_model = GenerationModel::FluxPro11Ultra;
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
prompt: request.prompt.clone(),
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
@@ -17,7 +17,7 @@ use tauri::AppHandle;
pub async fn handle_image_fal(
app: &AppHandle,
request: EnqueueTextToImageRequest,
request: &EnqueueTextToImageRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
@@ -27,7 +27,7 @@ use std::time::Duration;
use tauri::AppHandle;
pub async fn handle_gpt_image_1(
request: EnqueueTextToImageRequest,
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -28,7 +28,7 @@ use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::gen
use tauri::AppHandle;
pub async fn handle_gpt_image_1_artcraft(
request: EnqueueTextToImageRequest,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
@@ -22,7 +22,7 @@ use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_gpt_image_1_sora(
request: EnqueueTextToImageRequest,
request: &EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
@@ -39,7 +39,7 @@ use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_midjourney(
app: &AppHandle,
request: EnqueueTextToImageRequest,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
mj_creds_manager: &MidjourneyCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
@@ -1,4 +1,5 @@
pub mod canvas_background_removal_complete_event;
pub mod image_edit_complete_event;
pub mod refresh_account_state_event;
pub mod show_provider_login_modal_event;
pub mod show_provider_login_modal_event;
pub mod text_to_image_generation_complete_event;
@@ -0,0 +1,26 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use enums::tauri::ux::tauri_event_name::TauriEventName;
use serde_derive::Serialize;
use tokens::tokens::media_files::MediaFileToken;
use url::Url;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct TextToImageGenerationCompleteEvent {
pub generated_images: Vec<GeneratedImage>,
pub maybe_frontend_subscriber_id: Option<String>,
pub maybe_frontend_subscriber_payload: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct GeneratedImage {
pub media_token: MediaFileToken,
pub cdn_url: Url,
pub maybe_thumbnail_template: Option<String>,
}
impl BasicSendableEvent for TextToImageGenerationCompleteEvent {
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::TextToImageGenerationCompleteEvent;
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
}
@@ -19,7 +19,7 @@ use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use midjourney_client::client::midjourney_hostname::MidjourneyHostname;
use midjourney_client::credentials::midjourney_user_id::MidjourneyUserId;
use midjourney_client::endpoints::imagine::{imagine, ImagineRequest, MidjourneyJobType};
use midjourney_client::endpoints::imagine::{imagine, ImagineItem, ImagineRequest, MidjourneyJobType};
use midjourney_client::utils::get_image_url::get_image_url;
use midjourney_client::utils::image_downloader_client::ImageDownloaderClient;
use once_cell::sync::Lazy;
@@ -201,119 +201,18 @@ async fn check_midjourney_tasks(
None => continue,
};
let model_type = match midjourney_item.job_type {
Some(MidjourneyJobType::V6Diffusion) => ModelType::MidjourneyV6,
Some(MidjourneyJobType::V6p1Diffusion) => ModelType::MidjourneyV6p1,
Some(MidjourneyJobType::V6p1RawDiffusion) => ModelType::MidjourneyV6p1Raw,
Some(MidjourneyJobType::V7Diffusion) => ModelType::MidjourneyV7,
Some(MidjourneyJobType::V7RawDiffusion) => ModelType::MidjourneyV7Raw,
Some(MidjourneyJobType::V7DraftDiffusion) => ModelType::MidjourneyV7Draft,
Some(MidjourneyJobType::V7DraftRawDiffusion) => ModelType::MidjourneyV7DraftRaw,
Some(MidjourneyJobType::Other(ref other)) => {
info!("Unknown Midjourney job type (for job id {}): {}", midjourney_job_id, other);
ModelType::Midjourney
},
_ => ModelType::Midjourney,
};
let request = CreatePromptRequest {
uuid_idempotency_token: generate_random_uuid(),
positive_prompt: midjourney_item.full_command.clone(),
negative_prompt: None,
model_type: Some(model_type),
generation_provider: Some(GenerationProvider::Midjourney),
};
let prompt_response = create_prompt(
&app_env_configs.storyteller_host,
Some(storyteller_creds),
request
upload_midjourney_batch(
&app_handle,
&app_env_configs,
app_data_root,
task_database,
&storyteller_creds,
&image_downloader,
midjourney_job_id,
&local_task,
midjourney_item
).await?;
info!("Created prompt: {:?}", &prompt_response.prompt_token);
for index in 0..4 {
info!("Downloading generated Midjourney file...");
let download_path = download_midjourney_image(
&image_downloader,
midjourney_job_id,
index,
app_data_root
).await?;
let mut wait_delay = 0;
loop {
info!("Uploading to backend...");
// TODO: media_files.origin_category
// TODO: media_files.maybe_prompt_token
// TODO: media_files.maybe_generation_provider
// TODO: media_files.maybe_origin_model_type
// TODO: media_files.maybe_origin_model_token (sref?)
// TODO: media_files.is_batch_generated
// TODO: media_files.maybe_batch_token
// TODO: media_files.is_user_upload
// TODO: batch_generations.token
// TODO: batch_generations.entity_type
// TODO: batch_generations.entity_token
let result = upload_image_media_file_from_file(UploadImageFromFileArgs {
api_host: &app_env_configs.storyteller_host,
maybe_creds: Some(&storyteller_creds),
path: &download_path,
is_intermediate_system_file: false,
maybe_prompt_token: Some(&prompt_response.prompt_token),
}).await;
match result {
Ok(media_file) => {
info!("Successfully uploaded to backend: {:?}", media_file);
break;
},
Err(StorytellerError::Api(ApiError::TooManyRequests(_))) => {
error!("Too many requests, retrying upload after delay...");
// If we hit a rate limit, we can retry after a short delay.
wait_delay += 10;
if wait_delay > 60 {
wait_delay = 60;
}
tokio::time::sleep(std::time::Duration::from_secs(wait_delay)).await;
continue; // Retry the upload.
}
Err(err) => {
error!("Failed to upload to backend: {:?}", err);
return Err(err.into())
},
}
} // End loop
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
let updated = update_task_status(UpdateTaskArgs {
db: task_database.get_connection(),
task_id: &local_task.id,
status: TaskStatus::CompleteSuccess,
}).await?;
if !updated {
continue; // If anything breaks with queries, don't spam events.
}
let event = GenerationCompleteEvent {
//media_file_token: result.media_file_token,
action: Some(GenerationAction::GenerateImage),
service: GenerationServiceProvider::Midjourney,
model: None,
};
if let Err(err) = event.send(&app_handle) {
error!("Failed to send GenerationCompleteEvent: {:?}", err); // Fail open
}
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}
@@ -321,3 +220,131 @@ async fn check_midjourney_tasks(
Ok(())
}
async fn upload_midjourney_batch(
app_handle: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
task_database: &TaskDatabase,
storyteller_creds: &StorytellerCredentialSet,
image_downloader: &ImageDownloaderClient,
midjourney_job_id: &String,
local_task: &Task,
midjourney_item: &ImagineItem
) -> AnyhowResult<()> {
let model_type = match midjourney_item.job_type {
Some(MidjourneyJobType::V6Diffusion) => ModelType::MidjourneyV6,
Some(MidjourneyJobType::V6p1Diffusion) => ModelType::MidjourneyV6p1,
Some(MidjourneyJobType::V6p1RawDiffusion) => ModelType::MidjourneyV6p1Raw,
Some(MidjourneyJobType::V7Diffusion) => ModelType::MidjourneyV7,
Some(MidjourneyJobType::V7RawDiffusion) => ModelType::MidjourneyV7Raw,
Some(MidjourneyJobType::V7DraftDiffusion) => ModelType::MidjourneyV7Draft,
Some(MidjourneyJobType::V7DraftRawDiffusion) => ModelType::MidjourneyV7DraftRaw,
Some(MidjourneyJobType::Other(ref other)) => {
info!("Unknown Midjourney job type (for job id {}): {}", midjourney_job_id, other);
ModelType::Midjourney
},
_ => ModelType::Midjourney,
};
let request = CreatePromptRequest {
uuid_idempotency_token: generate_random_uuid(),
positive_prompt: midjourney_item.full_command.clone(),
negative_prompt: None,
model_type: Some(model_type),
generation_provider: Some(GenerationProvider::Midjourney),
};
let prompt_response = create_prompt(
&app_env_configs.storyteller_host,
Some(storyteller_creds),
request
).await?;
info!("Created prompt: {:?}", &prompt_response.prompt_token);
for index in 0..4 {
info!("Downloading generated Midjourney file...");
let download_path = download_midjourney_image(
&image_downloader,
midjourney_job_id,
index,
app_data_root
).await?;
let mut wait_delay = 0;
loop {
info!("Uploading to backend...");
// TODO: media_files.origin_category
// TODO: media_files.maybe_prompt_token
// TODO: media_files.maybe_generation_provider
// TODO: media_files.maybe_origin_model_type
// TODO: media_files.maybe_origin_model_token (sref?)
// TODO: media_files.is_batch_generated
// TODO: media_files.maybe_batch_token
// TODO: media_files.is_user_upload
// TODO: batch_generations.token
// TODO: batch_generations.entity_type
// TODO: batch_generations.entity_token
let result = upload_image_media_file_from_file(UploadImageFromFileArgs {
api_host: &app_env_configs.storyteller_host,
maybe_creds: Some(&storyteller_creds),
path: &download_path,
is_intermediate_system_file: false,
maybe_prompt_token: Some(&prompt_response.prompt_token),
}).await;
match result {
Ok(media_file) => {
info!("Successfully uploaded to backend: {:?}", media_file);
break;
},
Err(StorytellerError::Api(ApiError::TooManyRequests(_))) => {
error!("Too many requests, retrying upload after delay...");
// If we hit a rate limit, we can retry after a short delay.
wait_delay += 10;
if wait_delay > 60 {
wait_delay = 60;
}
tokio::time::sleep(std::time::Duration::from_secs(wait_delay)).await;
continue; // Retry the upload.
}
Err(err) => {
error!("Failed to upload to backend: {:?}", err);
return Err(err.into())
},
}
} // End loop
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
let updated = update_task_status(UpdateTaskArgs {
db: task_database.get_connection(),
task_id: &local_task.id,
status: TaskStatus::CompleteSuccess,
}).await?;
if !updated {
return Ok(()); // If anything breaks with queries, don't spam events.
}
let event = GenerationCompleteEvent {
//media_file_token: result.media_file_token,
action: Some(GenerationAction::GenerateImage),
service: GenerationServiceProvider::Midjourney,
model: None,
};
if let Err(err) = event.send(&app_handle) {
error!("Failed to send GenerationCompleteEvent: {:?}", err); // Fail open
}
Ok(())
}
@@ -37,7 +37,7 @@ pub async fn maybe_handle_inpainting_complete_event(
let job_result = match job.maybe_result {
Some(ref res) => res,
None => {
error!("Job result is None for task: {:?}", task);
warn!("Job result is None for task: {:?}", task);
return Ok(()); // No result, nothing to do
},
};
@@ -0,0 +1,123 @@
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::text_to_image_generation_complete_event::{GeneratedImage, TextToImageGenerationCompleteEvent};
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use anyhow::anyhow;
use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessionResultDetailsResponse};
use enums::tauri::tasks::task_type::TaskType;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use errors::AnyhowResult;
use log::{error, warn};
use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task};
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::media_files::list_batch_generated_redux_media_files::list_batch_generated_redux_media_files;
use tauri::AppHandle;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
pub async fn maybe_handle_text_to_image_complete_event(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
maybe_creds: Option<&StorytellerCredentialSet>,
task: &Task,
job: &ListSessionJobsItem,
) -> AnyhowResult<()> {
match task.task_type {
TaskType::ImageGeneration => {} // NB: Fall-through
_ => return Ok(()),
}
match task.frontend_caller {
Some(TauriCommandCaller::TextToImage) => {} // NB: Fall-through
_ => return Ok(()),
}
let job_result = match job.maybe_result {
Some(ref res) => res,
None => {
warn!("Job result is None for task: {:?}", task);
return Ok(()); // No result, nothing to do
},
};
let event = match job_result.maybe_batch_token.as_ref() {
Some(batch_token) => {
handle_batch(
app,
app_env_configs,
maybe_creds,
task,
job,
job_result,
batch_token
).await?
}
None => {
handle_single(
app,
task,
job,
job_result,
).await?
}
};
if let Err(err) = event.send(&app) {
error!("Failed to send TextToImageGenerationCompleteEvent: {:?}", err); // Fail open
}
Ok(())
}
pub async fn handle_single(
app: &AppHandle,
task: &Task,
job: &ListSessionJobsItem,
job_result: &ListSessionResultDetailsResponse,
) -> AnyhowResult<TextToImageGenerationCompleteEvent> {
Ok(TextToImageGenerationCompleteEvent {
generated_images: vec![GeneratedImage {
media_token: MediaFileToken::new_from_str(&job_result.entity_token),
cdn_url: job_result.media_links.cdn_url.clone(),
maybe_thumbnail_template: job_result.media_links.maybe_thumbnail_template.clone(),
}],
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
})
}
pub async fn handle_batch(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
maybe_creds: Option<&StorytellerCredentialSet>,
task: &Task,
job: &ListSessionJobsItem,
job_result: &ListSessionResultDetailsResponse,
batch_token: &BatchGenerationToken,
) -> AnyhowResult<TextToImageGenerationCompleteEvent> {
let result = list_batch_generated_redux_media_files(
&app_env_configs.storyteller_host,
maybe_creds,
batch_token,
).await?;
if result.media_files.is_empty() {
return Err(anyhow!("No media files found for batch token: {}", batch_token));
}
let media_files = result.media_files
.into_iter()
.map(|file| GeneratedImage {
media_token: file.token,
cdn_url: file.media_links.cdn_url,
maybe_thumbnail_template: file.media_links.maybe_thumbnail_template,
})
.collect();
Ok(TextToImageGenerationCompleteEvent {
generated_images: media_files,
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
})
}
@@ -1,2 +1,3 @@
pub mod maybe_handle_inpainting_complete_event;
pub mod maybe_handle_text_to_image_complete_event;
pub mod maybe_send_background_removal_complete_event;
@@ -12,8 +12,11 @@ use crate::core::utils::enum_conversion::task_type::to_generation_action;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::services::storyteller::threads::events::maybe_handle_inpainting_complete_event::maybe_handle_inpainting_complete_event;
use crate::services::storyteller::threads::events::maybe_handle_text_to_image_complete_event::maybe_handle_text_to_image_complete_event;
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::common::generation_provider::GenerationProvider;
use enums::common::job_status_plus::JobStatusPlus;
use enums::tauri::tasks::task_status::TaskStatus;
@@ -27,15 +30,13 @@ 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::jobs::list_session_jobs::{list_session_jobs, States};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tempdir::TempDir;
use artcraft_api_defs::jobs::list_session_jobs::ListSessionJobsItem;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::services::storyteller::threads::events::maybe_handle_inpainting_complete_event::maybe_handle_inpainting_complete_event;
pub async fn storyteller_task_polling_thread(
app_handle: AppHandle,
@@ -177,6 +178,18 @@ async fn send_additional_events(
) {
info!("Attempting to dispatch events for completed Storyteller : {:?}", task);
let result = maybe_handle_text_to_image_complete_event(
app_handle,
app_env_configs,
creds,
task,
job,
).await;
if let Err(err) = result {
error!("Failed to send text-to-image complete event: {:?}", err);
}
let result = maybe_handle_inpainting_complete_event(
app_handle,
app_env_configs,
@@ -16,6 +16,10 @@ pub enum TauriCommandCaller {
Canvas,
/// The inpainting editor
ImageEditor,
/// The text-to-image page
TextToImage,
/// The image-to-video page
ImageToVideo,
}
impl_enum_display_and_debug_using_to_str!(TauriCommandCaller);
@@ -29,6 +33,8 @@ impl TauriCommandCaller {
match self {
Self::Canvas => "canvas",
Self::ImageEditor => "image_editor",
Self::TextToImage => "text_to_image",
Self::ImageToVideo => "image_to_video",
}
}
@@ -36,6 +42,8 @@ impl TauriCommandCaller {
match job_status {
"canvas" => Ok(Self::Canvas),
"image_editor" => Ok(Self::ImageEditor),
"text_to_image" => Ok(Self::TextToImage),
"image_to_video" => Ok(Self::ImageToVideo),
_ => Err(format!("invalid tauri_command_caller: {:?}", job_status)),
}
}
@@ -46,6 +54,8 @@ impl TauriCommandCaller {
BTreeSet::from([
Self::Canvas,
Self::ImageEditor,
Self::TextToImage,
Self::ImageToVideo,
])
}
}
@@ -62,26 +72,34 @@ mod tests {
fn test_serialization() {
assert_serialization(TauriCommandCaller::Canvas, "canvas");
assert_serialization(TauriCommandCaller::ImageEditor, "image_editor");
assert_serialization(TauriCommandCaller::TextToImage, "text_to_image");
assert_serialization(TauriCommandCaller::ImageToVideo, "image_to_video");
}
#[test]
fn to_str() {
assert_eq!(TauriCommandCaller::Canvas.to_str(), "canvas");
assert_eq!(TauriCommandCaller::ImageEditor.to_str(), "image_editor");
assert_eq!(TauriCommandCaller::TextToImage.to_str(), "text_to_image");
assert_eq!(TauriCommandCaller::ImageToVideo.to_str(), "image_to_video");
}
#[test]
fn from_str() {
assert_eq!(TauriCommandCaller::from_str("canvas").unwrap(), TauriCommandCaller::Canvas);
assert_eq!(TauriCommandCaller::from_str("image_editor").unwrap(), TauriCommandCaller::ImageEditor);
assert_eq!(TauriCommandCaller::from_str("text_to_image").unwrap(), TauriCommandCaller::TextToImage);
assert_eq!(TauriCommandCaller::from_str("image_to_video").unwrap(), TauriCommandCaller::ImageToVideo);
}
#[test]
fn all_variants() {
let mut variants = TauriCommandCaller::all_variants();
assert_eq!(variants.len(), 2);
assert_eq!(variants.len(), 4);
assert_eq!(variants.pop_first(), Some(TauriCommandCaller::Canvas));
assert_eq!(variants.pop_first(), Some(TauriCommandCaller::ImageEditor));
assert_eq!(variants.pop_first(), Some(TauriCommandCaller::TextToImage));
assert_eq!(variants.pop_first(), Some(TauriCommandCaller::ImageToVideo));
assert_eq!(variants.pop_first(), None);
}
}
@@ -43,6 +43,11 @@ pub enum TauriEventName {
#[serde(rename = "canvas_bg_removed_event")]
CanvasBgRemovedEvent,
/// Special event:
/// Image edit is complete
#[serde(rename = "text_to_image_generation_complete_event")]
TextToImageGenerationCompleteEvent,
/// Special event:
/// Image edit is complete
#[serde(rename = "image_edit_complete_event")]
@@ -78,6 +83,7 @@ impl TauriEventName {
Self::GenerationCompleteEvent => "generation-complete-event",
Self::GenerationFailedEvent => "generation-failed-event",
Self::CanvasBgRemovedEvent => "canvas_bg_removed_event",
Self::TextToImageGenerationCompleteEvent => "text_to_image_generation_complete_event",
Self::ImageEditCompleteEvent => "image_edit_complete_event",
Self::RefreshAccountStateEvent => "refresh_account_state_event",
Self::ShowProviderLoginModalEvent => "show_provider_login_modal_event",
@@ -92,6 +98,7 @@ impl TauriEventName {
"generation-complete-event" => Ok(Self::GenerationCompleteEvent),
"generation-failed-event" => Ok(Self::GenerationFailedEvent),
"canvas_bg_removed_event" => Ok(Self::CanvasBgRemovedEvent),
"text_to_image_generation_complete_event" => Ok(Self::TextToImageGenerationCompleteEvent),
"image_edit_complete_event" => Ok(Self::ImageEditCompleteEvent),
"refresh_account_state_event" => Ok(Self::RefreshAccountStateEvent),
"show_provider_login_modal_event" => Ok(Self::ShowProviderLoginModalEvent),
@@ -109,6 +116,7 @@ impl TauriEventName {
Self::GenerationCompleteEvent,
Self::GenerationFailedEvent,
Self::CanvasBgRemovedEvent,
Self::TextToImageGenerationCompleteEvent,
Self::ImageEditCompleteEvent,
Self::RefreshAccountStateEvent,
Self::ShowProviderLoginModalEvent,
@@ -132,6 +140,7 @@ mod tests {
assert_serialization(TauriEventName::GenerationCompleteEvent, "generation-complete-event");
assert_serialization(TauriEventName::GenerationFailedEvent, "generation-failed-event");
assert_serialization(TauriEventName::CanvasBgRemovedEvent, "canvas_bg_removed_event");
assert_serialization(TauriEventName::TextToImageGenerationCompleteEvent, "text_to_image_generation_complete_event");
assert_serialization(TauriEventName::ImageEditCompleteEvent, "image_edit_complete_event");
assert_serialization(TauriEventName::RefreshAccountStateEvent, "refresh_account_state_event");
assert_serialization(TauriEventName::ShowProviderLoginModalEvent, "show_provider_login_modal_event");
@@ -145,6 +154,7 @@ mod tests {
assert_eq!(TauriEventName::GenerationCompleteEvent.to_str(), "generation-complete-event");
assert_eq!(TauriEventName::GenerationFailedEvent.to_str(), "generation-failed-event");
assert_eq!(TauriEventName::CanvasBgRemovedEvent.to_str(), "canvas_bg_removed_event");
assert_eq!(TauriEventName::TextToImageGenerationCompleteEvent.to_str(), "text_to_image_generation_complete_event");
assert_eq!(TauriEventName::ImageEditCompleteEvent.to_str(), "image_edit_complete_event");
assert_eq!(TauriEventName::RefreshAccountStateEvent.to_str(), "refresh_account_state_event");
assert_eq!(TauriEventName::ShowProviderLoginModalEvent.to_str(), "show_provider_login_modal_event");
@@ -158,6 +168,7 @@ mod tests {
assert_eq!(TauriEventName::from_str("generation-complete-event").unwrap(), TauriEventName::GenerationCompleteEvent);
assert_eq!(TauriEventName::from_str("generation-failed-event").unwrap(), TauriEventName::GenerationFailedEvent);
assert_eq!(TauriEventName::from_str("canvas_bg_removed_event").unwrap(), TauriEventName::CanvasBgRemovedEvent);
assert_eq!(TauriEventName::from_str("text_to_image_generation_complete_event").unwrap(), TauriEventName::TextToImageGenerationCompleteEvent);
assert_eq!(TauriEventName::from_str("image_edit_complete_event").unwrap(), TauriEventName::ImageEditCompleteEvent);
assert_eq!(TauriEventName::from_str("refresh_account_state_event").unwrap(), TauriEventName::RefreshAccountStateEvent);
assert_eq!(TauriEventName::from_str("show_provider_login_modal_event").unwrap(), TauriEventName::ShowProviderLoginModalEvent);
@@ -167,12 +178,13 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = TauriEventName::all_variants();
assert_eq!(variants.len(), 9);
assert_eq!(variants.len(), 10);
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationFailedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::CanvasBgRemovedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::TextToImageGenerationCompleteEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::ImageEditCompleteEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::RefreshAccountStateEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderLoginModalEvent));
@@ -14,6 +14,7 @@ interface TextToImageProps {
imageUrl?: string;
}
import { ModelInfo } from "@storyteller/model-list";
import { useTextToImageGenerationCompleteEvent } from "@storyteller/tauri-events";
const PAGE_ID : ModelPage = ModelPage.TextToImage;
@@ -36,6 +37,20 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
clearJobTokens: () => {},
};
useTextToImageGenerationCompleteEvent(async (event) => {
console.log("Text to image generation complete:", event);
// TODO: Logic to show edit history
for (const generatedImage of event.generated_images) {
console.log("Generated image:", generatedImage.media_token);
console.log("Generated image url:", generatedImage.cdn_url);
console.log(
"Generated image thumbnail template:",
generatedImage.maybe_thumbnail_template,
);
}
});
return (
<div
ref={containerRef}
@@ -278,6 +278,7 @@ export const PromptBoxImage = ({
model: modelInfo,
aspect_ratio: aspectRatio,
number_images: generationCount,
frontend_caller: "text_to_image",
});
console.log("PromptBoxImage - generateResponse", generateResponse);
@@ -18,6 +18,12 @@ export interface EnqueueTextToImageRequest {
model?: ModelInfo | EnqueueTextToImageModel;
aspect_ratio?: EnqueueTextToImageSize;
number_images?: number;
// TODO: Actual enum.
frontend_caller?: string;
// Optional frontend state to return later.
frontend_subscriber_id?: string;
}
// Shape of request sent to Tauri
@@ -27,6 +33,8 @@ interface EnqueueTextToImageRawRequest {
model?: EnqueueTextToImageModel | string; // TODO: Shouldn't allow string
aspect_ratio?: EnqueueTextToImageSize;
number_images?: number;
frontend_caller?: string;
frontend_subscriber_id?: string;
}
export enum EnqueueTextToImageModel {
@@ -83,6 +91,14 @@ export const EnqueueTextToImage = async (request: EnqueueTextToImageRequest) : P
mutableRequest.number_images = request.number_images;
}
if (!!request.frontend_caller) {
mutableRequest.frontend_caller = request.frontend_caller;
}
if (!!request.frontend_subscriber_id) {
mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id;
}
const result = await invoke("enqueue_text_to_image_command", {
request: mutableRequest,
});
+4 -4
View File
@@ -1,10 +1,10 @@
export * from './lib/common/BasicEventWrapper';
export * from './lib/events/FlashUserInputErrorEvent';
export * from './lib/events/RefreshAccountStateEvent';
export * from './lib/events/ShowProviderLoginModalEvent';
export * from './lib/events/functional/ImageEditCompleteEvent';
export * from './lib/events/functional/RefreshAccountStateEvent';
export * from './lib/events/functional/ShowProviderLoginModalEvent';
export * from './lib/events/functional/TextToImageGenerationCompleteEvent';
export * from './lib/events/generation/useGenerationCompleteEvent';
export * from './lib/events/generation/useGenerationEnqueueFailureEvent';
export * from './lib/events/generation/useGenerationEnqueueSuccessEvent';
export * from './lib/events/generation/useGenerationFailedEvent';
export * from './lib/tauri-events';
export * from './lib/events/warning/FlashUserInputErrorEvent';
@@ -1,5 +1,5 @@
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { BasicEventWrapper } from '../common/BasicEventWrapper';
import { BasicEventWrapper } from '../../common/BasicEventWrapper';
import { useEffect } from 'react';
const EVENT_NAME : string = 'refresh_account_state_event';
@@ -1,5 +1,5 @@
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { BasicEventWrapper } from '../common/BasicEventWrapper';
import { BasicEventWrapper } from '../../common/BasicEventWrapper';
import { useEffect } from 'react';
import { GenerationProvider } from '@storyteller/common';
@@ -0,0 +1,42 @@
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { BasicEventWrapper } from '../../common/BasicEventWrapper';
import { useEffect } from 'react';
const EVENT_NAME : string = 'text_to_image_generation_complete_event';
export interface TextToImageGenerationCompleteEvent {
generated_images: GeneratedImage[],
maybe_frontend_subscriber_id?: string,
maybe_frontend_subscriber_payload?: string,
}
export interface GeneratedImage {
media_token: string,
cdn_url: string,
maybe_thumbnail_template?: string,
}
export const useTextToImageGenerationCompleteEvent = (asyncCallback: (event: TextToImageGenerationCompleteEvent) => Promise<void>) => {
useEffect(() => {
let isUnmounted = false;
let unlisten: Promise<UnlistenFn>;
const setup = async () => {
unlisten = listen<BasicEventWrapper<TextToImageGenerationCompleteEvent>>(EVENT_NAME, async (wrappedEvent) => {
await asyncCallback(wrappedEvent.payload.data);
});
if (isUnmounted) {
unlisten.then(f => f()); // Unsubscribe if unmounted early.
}
};
setup();
return () => {
isUnmounted = true;
unlisten.then(f => f());
};
}, []);
}
@@ -1,5 +1,5 @@
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { BasicEventWrapper } from '../common/BasicEventWrapper';
import { BasicEventWrapper } from '../../common/BasicEventWrapper';
import { useEffect } from 'react';
const EVENT_NAME : string = 'flash_user_input_error_event';
@@ -1,7 +0,0 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -1,12 +0,0 @@
import { render } from '@testing-library/react';
import FrontendTauriEvents from './tauri-events';
describe('FrontendTauriEvents', () => {
it('should render successfully', () => {
const { baseElement } = render(<FrontendTauriEvents />);
expect(baseElement).toBeTruthy();
});
});
@@ -1,15 +0,0 @@
import styles from './tauri-events.module.css';
export function FrontendTauriEvents() {
return (
<div className={styles['container']}>
<h1>Welcome to FrontendTauriEvents!</h1>
</div>
);
}
export default FrontendTauriEvents;