Compare commits

...
Author SHA1 Message Date
Brandon Thomas c0a24b913d cleanup 2025-08-03 15:58:49 -04:00
Brandon Thomas 1bc275293b masking and inpainting works 2025-08-03 15:45:09 -04:00
Brandon Thomas 4521088453 communication to fal works, mask is wrong color 2025-08-03 15:05:17 -04:00
Brandon Thomas 0bdf79dfe4 wip 2025-08-03 14:45:37 -04:00
Brandon Thomas 770b361b94 reset default tab 2025-08-03 12:32:29 -04:00
Brandon Thomas 32282d628f wip 2025-08-02 13:38:14 -04:00
Brandon Thomas 6294d4b71c semi working 2025-08-02 13:38:14 -04:00
Brandon Thomas ac3ed99615 wip 2025-08-02 13:38:14 -04:00
Brandon Thomas 4722bc3fec cleanup 2025-08-02 13:38:14 -04:00
Brandon Thomas 84a32d1c36 handle raw bytes (wip) 2025-08-02 13:38:14 -04:00
24 changed files with 525 additions and 57 deletions
Generated
+1
View File
@@ -663,6 +663,7 @@ dependencies = [
"futures 0.3.31",
"idempotency",
"image",
"images",
"indexmap 2.9.0",
"log",
"memory_store",
@@ -1,4 +1,5 @@
pub mod get_media_file;
pub mod upload_image_media_file_from_bytes;
pub mod upload_image_media_file_from_file;
pub mod upload_new_engine_asset_from_file;
pub mod upload_video_media_file_from_file;
@@ -0,0 +1,99 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::http_get_anonymous::http_get_anonymous;
use chrono::{DateTime, Utc};
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
use idempotency::uuid::generate_random_uuid;
use log::debug;
use reqwest::multipart::{Form, Part};
use reqwest::Client;
use serde_derive::{Deserialize, Serialize};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::prompts::PromptToken;
use uuid::uuid;
/// Upload an image media file from a file.
/// NB: We need owned bytes for the request.
pub async fn upload_image_media_file_from_bytes(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
bytes: Vec<u8>,
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
let url = get_route(api_host);
debug!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let part = Part::bytes(bytes)
.file_name("image.png")
.mime_str("image/png")
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let form = Form::new()
.text("uuid_idempotency_token", generate_random_uuid())
.part("file", part);
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let response = request_builder
.multipart(form)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response).await?;
let response_body = &response.text().await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let media_file = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(media_file)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname_and_scheme = api_host.to_api_hostname_and_scheme();
format!("{}/v1/media_files/upload/image", api_hostname_and_scheme)
}
// TODO(bt,2025-04-22): Share API definitions between client and server in common crate.
#[derive(Deserialize, Debug)]
pub struct UploadImageMediaFileSuccessResponse {
pub success: bool,
pub media_file_token: MediaFileToken,
}
+1
View File
@@ -33,6 +33,7 @@ errors = { workspace = true }
fal_client = { workspace = true }
filesys = { workspace = true }
idempotency = { workspace = true }
images = { workspace = true }
memory_store = { workspace = true }
mimetypes = { workspace = true }
openai_sora_client = { workspace = true }
@@ -60,17 +60,16 @@ pub struct EnqueueInpaintImageCommand {
pub model: Option<ImageInpaintModel>,
/// REQUIRED (Option<T> is just for error messages).
/// Images to use for the image edit.
/// The first image is typically a 2D canvas or 3D stage, but doesn't have to be.
/// There must be at least one image.
/// The source image to edit.
pub image_media_token: Option<MediaFileToken>,
// TODO: Allow for bytes.
/// REQUIRED (Option<T> is just for error messages).
/// Images to use for the image edit.
/// The first image is typically a 2D canvas or 3D stage, but doesn't have to be.
/// There must be at least one image.
pub mask_media_token: Option<MediaFileToken>,
/// REQUIRED: Supply this *XOR* `mask_image_raw_bytes`.
/// The mask to focus the edit (already uploaded).
pub mask_image_media_token: Option<MediaFileToken>,
/// REQUIRED: Supply this *XOR* `mask_image_media_token`.
/// The mask to focus the edit (raw bytes).
pub mask_image_raw_bytes: Option<Vec<u8>>,
/// REQUIRED.
/// The user's image generation prompt.
@@ -78,6 +77,9 @@ pub struct EnqueueInpaintImageCommand {
/// Number of images to generate.
pub image_count: Option<u32>,
/// If true, force the dimensions of the source image and mask image to match.
pub require_matching_dimensions: Option<bool>,
}
#[derive(Serialize, Debug)]
@@ -95,20 +97,17 @@ pub enum EnqueueInpaintImageErrorType {
/// No mask image was supplied.
NoMaskImageSpecified,
/// Too many mask images were supplied.
MultipleMaskImagesSpecified,
/// Bad mask image was supplied.
BadMaskImage,
/// Generic bad request error
BadRequest,
/// Generic server error
ServerError,
/// The user is sending too many requests
TooManyConcurrentTasks,
/// User is not logged into Sora!
SoraLoginRequired,
/// The user needs to create a Sora account
SoraUsernameNotYetCreated,
/// The Sora service is having problems. Try again soon.
SoraIsHavingProblems,
}
#[derive(Serialize)]
@@ -132,8 +131,8 @@ pub async fn enqueue_image_inpaint_command(
sora_task_queue: State<'_, SoraTaskQueue>,
) -> ResponseOrErrorType<EnqueueImageInpaintSuccessResponse, EnqueueInpaintImageErrorType> {
info!("enqueue_image_inpaint_command called; full request: {:?}",
&request);
info!("enqueue_image_inpaint_command called; model: {:?}, image_media_token: {:?}, prompt: {:?}",
&request.model, &request.image_media_token, &request.prompt);
let result = handle_request(
&request,
@@ -17,7 +17,16 @@ pub enum InternalImageInpaintError {
/// A mask image was not provided
NoMaskImageSpecified,
/// Both mask image media token and raw bytes were supplied.
MaskMediaTokenAndBytesSupplied,
/// Mask image has an unknown MIME type.
CouldNotDetermineMaskMimeType,
/// Could not encode the mask image to PNG.
CouldNotEncodeMask,
/// No storyteller credentials are set.
NeedsStorytellerCredentials,
@@ -76,7 +85,22 @@ impl InternalImageInpaintError {
InternalImageInpaintError::NoMaskImageSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::NoMaskImageSpecified;
error_message = "No source image was provided".to_string();
error_message = "No mask image was provided".to_string();
}
InternalImageInpaintError::MaskMediaTokenAndBytesSupplied => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::MultipleMaskImagesSpecified;
error_message = "multiple mask images provided".to_string();
}
InternalImageInpaintError::CouldNotDetermineMaskMimeType => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::BadMaskImage;
error_message = "bad mask image (mime)".to_string();
}
InternalImageInpaintError::CouldNotEncodeMask => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::BadMaskImage;
error_message = "bad mask image (encode)".to_string();
}
InternalImageInpaintError::InvalidNumberOfRequestedImages { min, max, requested } => {
status = CommandErrorStatus::BadRequest;
@@ -1,5 +1,7 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
@@ -12,6 +14,7 @@ use crate::core::model::image_models::ImageModel;
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;
@@ -29,14 +32,23 @@ 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::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
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;
use images::mask_images::normalize_image_bytes_to_flux_mask::normalize_image_bytes_to_flux_mask;
use log::{error, info};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
use storyteller_client::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::media_files::upload_image_media_file_from_bytes::upload_image_media_file_from_bytes;
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_flux_pro_1_inpaint_artcraft(
request: &EnqueueInpaintImageCommand,
@@ -53,10 +65,31 @@ pub async fn handle_flux_pro_1_inpaint_artcraft(
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(InternalImageInpaintError::NoSourceImageSpecified);
},
};
//let image_media_file = get_media_file(
// &app_env_configs.storyteller_host,
// &image_media_token,
//).await.map_err(|err| {
// error!("Failed to get media file for image: {:?}", err);
// InternalImageInpaintError::StorytellerError(err)
//})?;
let mask_media_token = get_mask(
request,
app_env_configs,
&creds,
).await?;
info!("Calling Artcraft flux pro 1 inpaint ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.image_count {
None => None,
Some(1) => Some(FluxPro1InpaintImageNumImages::One),
@@ -71,20 +104,6 @@ pub async fn handle_flux_pro_1_inpaint_artcraft(
});
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(InternalImageInpaintError::NoSourceImageSpecified);
},
};
let mask_media_token = match &request.mask_media_token {
Some(token) => token.clone(),
None => {
return Err(InternalImageInpaintError::NoMaskImageSpecified);
},
};
let request = FluxPro1InpaintImageRequest {
uuid_idempotency_token,
@@ -120,3 +139,41 @@ pub async fn handle_flux_pro_1_inpaint_artcraft(
task_type: TaskType::ImageGeneration,
})
}
async fn get_mask(
request: &EnqueueInpaintImageCommand,
app_env_configs: &AppEnvConfigs,
storyteller_creds: &StorytellerCredentialSet,
) -> Result<MediaFileToken, InternalImageInpaintError> {
if request.mask_image_media_token.is_some() && request.mask_image_raw_bytes.is_some() {
return Err(InternalImageInpaintError::MaskMediaTokenAndBytesSupplied);
}
if let Some(token) = request.mask_image_media_token.as_ref() {
return Ok(token.clone());
};
let image_bytes = request.mask_image_raw_bytes.as_ref()
.ok_or(InternalImageInpaintError::NoMaskImageSpecified)?;
let image_bytes = normalize_image_bytes_to_flux_mask(image_bytes)
.map_err(|err| {
error!("Failed to convert image bytes to png: {:?}", err);
InternalImageInpaintError::CouldNotEncodeMask
})?;
info!("Uploading image media file from bytes...");
let result = upload_image_media_file_from_bytes(
&app_env_configs.storyteller_host,
Some(&storyteller_creds),
image_bytes.0
).await
.map_err(|err| {
error!("Failed to upload image media file: {:?}", err);
InternalImageInpaintError::StorytellerError(err)
})?;
Ok(result.media_file_token)
}
+2
View File
@@ -7,6 +7,7 @@ use crate::core::commands::app_preferences::get_app_preferences_command::get_app
use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::enqueue_text_to_image_command;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::enqueue_image_bg_removal_command;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::enqueue_image_inpaint_command;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::enqueue_contextual_edit_image_command;
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command;
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
@@ -144,6 +145,7 @@ pub fn run() {
check_sora_session_command,
enqueue_contextual_edit_image_command,
enqueue_image_bg_removal_command,
enqueue_image_inpaint_command,
enqueue_image_to_3d_object_command,
enqueue_image_to_video_command,
enqueue_text_to_image_command,
@@ -0,0 +1,18 @@
use crate::error::ImagesError;
use image::{ImageFormat, ImageReader};
use std::io::Cursor;
pub fn image_bytes_to_png_bytes(
arbitrary_image_bytes: &[u8],
) -> Result<Vec<u8>, ImagesError> {
let reader = ImageReader::new(Cursor::new(arbitrary_image_bytes));
let image = reader
.with_guessed_format()? // NB: Can raise IoError.
.decode()?;
let mut output_bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut output_bytes), ImageFormat::Png)?;
Ok(output_bytes)
}
@@ -0,0 +1,29 @@
use std::io::Cursor;
use image::{ImageFormat, ImageReader};
use crate::error::ImagesError;
pub struct PngBytesWithDimensions {
pub png_bytes: Vec<u8>,
pub width: u32,
pub height: u32,
}
pub fn image_bytes_to_png_bytes_with_dimensions(
arbitrary_image_bytes: &[u8],
) -> Result<PngBytesWithDimensions, ImagesError> {
let reader = ImageReader::new(Cursor::new(arbitrary_image_bytes));
let image = reader
.with_guessed_format()? // NB: Can raise IoError.
.decode()?;
let mut output_bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut output_bytes), ImageFormat::Png)?;
Ok(PngBytesWithDimensions {
png_bytes: output_bytes,
width: image.width(),
height: image.height(),
})
}
@@ -1 +1,3 @@
pub mod image_bytes_to_png_bytes;
pub mod image_bytes_to_png_bytes_with_dimensions;
pub mod webp_bytes_to_png_bytes;
+1
View File
@@ -26,6 +26,7 @@ pub use image;
pub mod encoding;
pub mod error;
pub mod image_info;
pub mod mask_images;
pub mod resize_image_file_preserving_aspect;
pub mod resize_preserving_aspect;
@@ -0,0 +1 @@
pub mod normalize_image_bytes_to_flux_mask;
@@ -0,0 +1,40 @@
use crate::error::ImagesError;
use image::{GenericImage, GenericImageView, ImageFormat, ImageReader, Rgba};
use std::io::Cursor;
pub struct PngBytes(pub Vec<u8>);
/// Normalize arbitrary image bytes to a PNG format image.
/// Make sure the bitmap is in the correct mask format.
pub fn normalize_image_bytes_to_flux_mask(
arbitrary_image_bytes: &[u8],
) -> Result<PngBytes, ImagesError> {
let reader = ImageReader::new(Cursor::new(arbitrary_image_bytes));
let mut image = reader
.with_guessed_format()? // NB: Can raise IoError.
.decode()?;
let (width, height) = image.dimensions();
for y in 0..height {
for x in 0..width {
let rgba_pixel = image.get_pixel(x, y);
if rgba_pixel[3] == 0 {
// Alpha pixel is transparent
// Set to black. Mask ignore region.
image.put_pixel(x, y, Rgba([0, 0, 0, 255]));
} else {
// Set to white. Mask interest region.
image.put_pixel(x, y, Rgba([255, 255, 255, 255]));
}
}
}
let mut output_bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut output_bytes), ImageFormat::Png)?;
Ok(PngBytes(output_bytes))
}
@@ -1,3 +1,4 @@
export const normalizeCanvas = (canvas: HTMLCanvasElement, width: number, height: number): HTMLCanvasElement => {
const newCanvas = document.createElement("canvas");
newCanvas.width = width;
@@ -0,0 +1,16 @@
export const EncodeImageBitmapToArray = async (imageBitmap: ImageBitmap): Promise<Uint8Array> => {
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error("Failed to get 2D context");
}
ctx.drawImage(imageBitmap, 0, 0);
const blob = await canvas.convertToBlob({ type: 'image/png' });
const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer);
}
@@ -8,6 +8,8 @@ import Konva from "konva"; // just for types
import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap"
import {
EnqueueImageInpaint,
EnqueueImageInpaintModel,
FalBackgroundRemoval,
} from "@storyteller/tauri-api";
import { ContextMenuContainer } from "../PageDraw/components/ui/ContextMenu";
@@ -188,7 +190,7 @@ const PageEdit = () => {
}
// Create a function to use the left layer ref and download the bitmap from it
const downloadLeftPanelBitmap = () => {
const downloadLeftPanelBitmap = async(): Promise<Uint8Array> => {
if (!stageRef.current || !leftPanelRef.current || !rectRef.current) {
console.error("Stage or left panel ref is not available");
return;
@@ -206,24 +208,63 @@ const PageEdit = () => {
pixelRatio: 1 / stageRef.current.scaleX(),
});
// Using the pixelRatio scaling may result in off-by-one rounding errors,
// So we re-fit the image to a canvas of precise size.
const fittedCanvas = normalizeCanvas(layerCrop, rect.width(), rect.height());
// // Using the pixelRatio scaling may result in off-by-one rounding errors,
// // So we re-fit the image to a canvas of precise size.
// const fittedCanvas = normalizeCanvas(layerCrop, rect.width(), rect.height());
// const downloadCallback = (blob: Blob | null) => {
// if (!blob) {
// console.error("Failed to create blob from canvas");
// return;
// }
// const url = URL.createObjectURL(blob);
// const link = document.createElement("a");
// link.href = url;
// link.download = "artcraft_snapshot.png";
// link.click();
// console.log('downloadCallback', url);
// }
// fittedCanvas.toBlob(downloadCallback, "image/png", 1.0);
const downloadCallback = (blob: Blob | null) => {
if (!blob) {
console.error("Failed to create blob from canvas");
return;
const normalizeCanvas2 = (canvas: HTMLCanvasElement, width: number, height: number): OffscreenCanvas => {
const newCanvas = new OffscreenCanvas(width, height);
const ctx = newCanvas.getContext("2d");
if (!ctx) {
throw new Error("Failed to get canvas context");
}
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "artcraft_snapshot.png";
link.click();
ctx.imageSmoothingEnabled = true;
ctx.drawImage(canvas, 0, 0, width, height);
return newCanvas;
}
fittedCanvas.toBlob(downloadCallback, "image/png", 1.0);
const fittedCanvas2 = normalizeCanvas2(layerCrop, rect.width(), rect.height());
const blob = await fittedCanvas2.convertToBlob({ type: 'image/png' });
const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer);
};
const handleGenerate = async (prompt: string) => {
const editedImageToken = store.baseImageInfo?.mediaToken;
if (!editedImageToken) {
console.error("Base image is not available");
return;
}
// TODO: Call inference API here
let arrayBuffer = await downloadLeftPanelBitmap();
const response = await EnqueueImageInpaint({
model: EnqueueImageInpaintModel.FluxPro1,
image_media_token: editedImageToken,
mask_image_raw_bytes: arrayBuffer,
prompt: prompt,
image_count: 1,
});
};
// Display image selector on launch, otherwise hide it
@@ -262,7 +303,7 @@ const PageEdit = () => {
<PromptEditor
onModeChange={(mode: string) => { store.setActiveTool(mode as ActiveEditTool) }}
selectedMode={store.activeTool}
onGenerateClick={downloadLeftPanelBitmap}
onGenerateClick={handleGenerate}
/>
</div>
<div className="relative z-0">
@@ -3,6 +3,7 @@ import { PromptBoxEdit, PromptBoxEditProps } from "@storyteller/ui-promptbox";
// Set this value on when enqueue is pressed nasty global variable.
import { JobProvider } from "~/pages/PageDraw/JobContext";
const PromptEditor: React.FC<PromptBoxEditProps> = ({
onModeChange,
selectedMode,
@@ -3,6 +3,8 @@ import { useSceneStore } from '../PageDraw/stores/SceneState';
export type TabId = '2D' | '3D' | 'VIDEO' | 'IMAGE' | 'EDIT';
const DEFAULT_TAB : TabId = 'EDIT';
interface TabState {
// Current active tab
activeTabId: TabId;
@@ -18,7 +20,7 @@ interface TabState {
}
export const useTabStore = create<TabState>((set, get) => ({
activeTabId: 'EDIT',
activeTabId: DEFAULT_TAB,
tabData: {},
setActiveTab: async (newTabId) => {
@@ -895,8 +895,6 @@ export const Modal = ({
>
{/* Animated Modal Content */}
{transitions((styles, item) => {
// Debug logging - remove this after testing
console.log("Modal animation styles:", styles);
return item ? (
<Dialog.Content
forceMount
@@ -42,6 +42,8 @@ import {
EnqueueContextualEditImage,
EnqueueContextualEditImageModel,
EnqueueContextualEditImageSize,
EnqueueImageInpaint,
EnqueueImageInpaintModel,
} from "@storyteller/tauri-api";
// import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
// import { invoke } from "@tauri-apps/api/core";
@@ -333,6 +335,41 @@ export const PromptBox2D = ({
});
console.log("generateResponse", generateResponse);
// NB: Tanish, this is an example request to inpainting
// NB: Tanish, this is an example request to inpainting
// NB: Tanish, this is an example request to inpainting
// NB: Tanish, this is an example request to inpainting
const EncodeImageBitmapToArray = async (imageBitmap: ImageBitmap): Promise<Uint8Array> => {
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
// Convert canvas to PNG Blob
const blob = await canvas.convertToBlob({ type: 'image/png' });
// Read Blob as ArrayBuffer
const arrayBuffer = await blob.arrayBuffer();
// Convert to Uint8Array
return new Uint8Array(arrayBuffer);
}
const uint8Array = await EncodeImageBitmapToArray(image);
const generateResponse2 = await EnqueueImageInpaint({
model: EnqueueImageInpaintModel.FluxPro1,
image_media_token: "m_9y8hy50582mwxpm8hxmh4n4wjeh5cr",
mask_image_raw_bytes: uint8Array,
prompt: prompt,
image_count: 1,
});
// NB: Tanish, end example.
// NB: Tanish, end example.
// NB: Tanish, end example.
// NB: Tanish, end example.
};
const handleWebEnqueue = async () => {
@@ -390,6 +427,7 @@ export const PromptBox2D = ({
toast.error("Failed to enqueue image generation. Please try again.");
}
};
const handleEnqueue = async () => {
console.log("Pressing Enqueue");
if (onEnqueuePressed) {
@@ -17,7 +17,7 @@ import { useEffect, useRef, useState } from "react";
export interface PromptBoxEditProps {
onModeChange?: (mode: string) => void;
selectedMode?: string;
onGenerateClick: () => void;
onGenerateClick: (prompt: string) => void;
isDisabled?: boolean;
}
@@ -132,7 +132,7 @@ export const PromptBoxEdit = ({
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isDisabled ? faSparkles : undefined}
onClick={onGenerateClick}
onClick={() => onGenerateClick(prompt)}
disabled={isDisabled || !prompt.trim()}
>
{isDisabled ? (
+1
View File
@@ -3,6 +3,7 @@ export * from "./lib/enqueue/EnqueueContextualEditImage.js";
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
export * from "./lib/enqueue/EnqueueTextToImage.js";
export * from "./lib/enqueue/EnqueueImageToVideo.js";
export * from "./lib/enqueue/EnqueueImageInpaint.js";
export * from "./lib/fal/FalBackgroundRemoval.js";
export * from "./lib/fal/FalHunyuanImageTo3d.js";
export * from "./lib/fal/FalKlingImageToVideo.js";
@@ -0,0 +1,95 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
import { ModelInfo } from "@storyteller/model-list";
export interface EnqueueImageInpaintRequest {
// The model to use.
model?: ModelInfo | EnqueueImageInpaintModel;
// The image we're editing.
image_media_token?: string;
// The mask to focus the edit (raw bytes).
mask_image_raw_bytes?: Uint8Array;
// The user's image generation prompt.
prompt?: string;
// Number of images to generate.
image_count?: number;
}
// Shape of request sent to Tauri
// (We do some transformations from the public-facing request object.)
interface RawEnqueueImageInpaintRequest {
model?: EnqueueImageInpaintModel | string;
image_media_token?: string;
mask_image_raw_bytes?: Uint8Array;
prompt?: string;
image_count?: number;
}
export enum EnqueueImageInpaintErrorType {
ModelNotSpecified = "model_not_specified",
NoProviderAvailable = "no_provider_available",
BadRequest = "bad_request",
ServerError = "server_error",
TooManyConcurrentTasks = "too_many_concurrent_tasks",
SoraLoginRequired = "sora_login_required",
SoraUsernameNotYetCreated = "sora_username_not_yet_created",
SoraIsHavingProblems = "sora_is_having_problems",
}
export enum EnqueueImageInpaintModel {
FluxPro1 = "flux_pro_1",
}
export interface EnqueueImageInpaintError extends CommandResult {
error_type: EnqueueImageInpaintErrorType;
error_message?: string;
}
export interface EnqueueImageInpaintPayload {
}
export interface EnqueueImageInpaintSuccess extends CommandResult {
payload: EnqueueImageInpaintPayload;
}
export type EnqueueImageInpaintResult = EnqueueImageInpaintSuccess | EnqueueImageInpaintError;
export const EnqueueImageInpaint = async (request: EnqueueImageInpaintRequest) : Promise<EnqueueImageInpaintResult> => {
let modelName = undefined;
if (!!request.model) {
if (typeof request.model === "string") {
modelName = request.model;
} else {
modelName = request.model.tauri_id;
}
}
let mutableRequest : RawEnqueueImageInpaintRequest = {
model: modelName,
prompt: request.prompt,
};
if (!!request.image_media_token) {
mutableRequest.image_media_token = request.image_media_token;
}
if (!!request.mask_image_raw_bytes) {
mutableRequest.mask_image_raw_bytes = request.mask_image_raw_bytes;
}
if (!!request.image_count) {
mutableRequest.image_count = request.image_count;
}
const result = await invoke("enqueue_image_inpaint_command", {
request: mutableRequest,
});
return (result as EnqueueImageInpaintResult);
}