Fix Imaginate by porting its JS roundtrip code to graph-based async execution in Rust (#1250)

* Create asynchronous rust imaginate node

* Make a first imaginate request via rust

* Implement parsing of imaginate API result image

* Stop refresh timer from affecting imaginate progress requests

* Add cargo-about clarification for rustls-webpki

* Delete imaginate.ts and all uses of its functions

* Add imaginate img2img feature

* Fix imaginate random seed button

* Fix imaginate ui inferring non-custom resolutions

* Fix the imaginate progress indicator

* Remove ImaginatePreferences from being compiled into node graph

* Regenerate imaginate only when hitting button

* Add ability to terminate imaginate requests

* Add imaginate server check feature

* Do not compile wasm_bindgen bindings in graphite_editor for tests

* Address some review suggestions

- move wasm futures dependency in editor to the future-executor crate
- guard wasm-bindgen in editor behind a `wasm` feature flag
- dont make seed number input a slider
- remove poll_server_check from process_message function beginning
- guard wasm related code behind `cfg(target_arch = "wasm32")` instead
  of `cfg(test)`
- Call the imaginate idle states "Ready" and "Done" instead of "Nothing
  to do"
- Call the imaginate uploading state "Uploading Image" instead of
  "Uploading Input Image"
- Remove the EvalSyncNode

* Fix imaginate host name being restored between graphite instances

also change the progress status texts a bit.

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
nat-rix
2023-06-09 09:03:15 +02:00
committed by Keavon Chambers
parent a1c70c4d90
commit f76b850b9c
35 changed files with 1500 additions and 1326 deletions

View File

@@ -12,4 +12,9 @@ futures = "0.3.25"
log = "0.4"
[target.wasm32-unknown-unknown.dependencies]
wasm-rs-async-executor = {version = "0.9.0", features = ["cooperative-browser", "debug", "requestIdleCallback"] }
wasm-rs-async-executor = { version = "0.9.0", features = [
"cooperative-browser",
"debug",
"requestIdleCallback",
] }
wasm-bindgen-futures = "0.4.36"

View File

@@ -22,3 +22,8 @@ pub fn block_on<F: Future + 'static>(future: F) -> F::Output {
#[cfg(not(target_arch = "wasm32"))]
futures::executor::block_on(future)
}
#[cfg(target_arch = "wasm32")]
pub fn spawn<F: Future<Output = ()> + 'static>(future: F) {
wasm_bindgen_futures::spawn_local(future);
}

View File

@@ -112,10 +112,25 @@ impl<T: ApplicationIo> ApplicationIo for &T {
}
}
#[derive(Debug, Clone)]
pub enum NodeGraphUpdateMessage {
ImaginateStatusUpdate,
}
pub trait NodeGraphUpdateSender {
fn send(&self, message: NodeGraphUpdateMessage);
}
pub trait GetImaginatePreferences {
fn get_host_name(&self) -> &str;
}
pub struct EditorApi<'a, Io> {
pub image_frame: Option<ImageFrame<Color>>,
pub font_cache: &'a FontCache,
pub application_io: &'a Io,
pub node_graph_message_sender: &'a dyn NodeGraphUpdateSender,
pub imaginate_preferences: &'a dyn GetImaginatePreferences,
}
impl<'a, Io> Clone for EditorApi<'a, Io> {
@@ -124,6 +139,8 @@ impl<'a, Io> Clone for EditorApi<'a, Io> {
image_frame: self.image_frame.clone(),
font_cache: self.font_cache,
application_io: self.application_io,
node_graph_message_sender: self.node_graph_message_sender,
imaginate_preferences: self.imaginate_preferences,
}
}
}

View File

@@ -441,7 +441,7 @@ impl NodeNetwork {
}
/// Check if the specified node id is connected to the output
pub fn connected_to_output(&self, target_node_id: NodeId, ignore_imaginate: bool) -> bool {
pub fn connected_to_output(&self, target_node_id: NodeId) -> bool {
// If the node is the output then return true
if self.outputs.iter().any(|&NodeOutput { node_id, .. }| node_id == target_node_id) {
return true;
@@ -454,11 +454,6 @@ impl NodeNetwork {
already_visited.extend(self.outputs.iter().map(|output| output.node_id));
while let Some(node) = stack.pop() {
// Skip the imaginate node inputs
if ignore_imaginate && node.name == "Imaginate" {
continue;
}
for input in &node.inputs {
if let &NodeInput::Node { node_id: ref_id, .. } = input {
// Skip if already viewed
@@ -680,7 +675,7 @@ impl NodeNetwork {
let mut dummy_input = NodeInput::ShortCircut(concrete!(()));
std::mem::swap(&mut dummy_input, input);
if let NodeInput::Value { tagged_value, exposed } = dummy_input {
if let NodeInput::Value { mut tagged_value, exposed } = dummy_input {
let value_node_id = gen_id();
let merged_node_id = map_ids(id, value_node_id);
let path = if let Some(mut new_path) = node.path.clone() {

View File

@@ -1,6 +1,6 @@
use super::DocumentNode;
use crate::graphene_compiler::Any;
pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus};
pub use crate::imaginate_input::{ImaginateCache, ImaginateController, ImaginateMaskStartingFill, ImaginateSamplingMethod};
use crate::proto::{Any as DAny, FutureAny};
use graphene_core::raster::brush_cache::BrushCache;
@@ -27,7 +27,7 @@ pub enum TaggedValue {
OptionalDVec2(Option<DVec2>),
DAffine2(DAffine2),
Image(graphene_core::raster::Image<Color>),
RcImage(Option<Arc<graphene_core::raster::Image<Color>>>),
ImaginateCache(ImaginateCache),
ImageFrame(graphene_core::raster::ImageFrame<Color>),
Color(graphene_core::raster::color::Color),
Subpaths(Vec<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>),
@@ -36,7 +36,7 @@ pub enum TaggedValue {
LuminanceCalculation(LuminanceCalculation),
ImaginateSamplingMethod(ImaginateSamplingMethod),
ImaginateMaskStartingFill(ImaginateMaskStartingFill),
ImaginateStatus(ImaginateStatus),
ImaginateController(ImaginateController),
LayerPath(Option<Vec<u64>>),
VectorData(graphene_core::vector::VectorData),
Fill(graphene_core::vector::style::Fill),
@@ -83,7 +83,7 @@ impl Hash for TaggedValue {
}
Self::DAffine2(m) => m.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)),
Self::Image(i) => i.hash(state),
Self::RcImage(i) => i.hash(state),
Self::ImaginateCache(i) => i.hash(state),
Self::Color(c) => c.hash(state),
Self::Subpaths(s) => s.iter().for_each(|subpath| subpath.hash(state)),
Self::RcSubpath(s) => s.hash(state),
@@ -91,7 +91,7 @@ impl Hash for TaggedValue {
Self::LuminanceCalculation(l) => l.hash(state),
Self::ImaginateSamplingMethod(m) => m.hash(state),
Self::ImaginateMaskStartingFill(f) => f.hash(state),
Self::ImaginateStatus(s) => s.hash(state),
Self::ImaginateController(s) => s.hash(state),
Self::LayerPath(p) => p.hash(state),
Self::ImageFrame(i) => i.hash(state),
Self::VectorData(vector_data) => vector_data.hash(state),
@@ -146,7 +146,7 @@ impl<'a> TaggedValue {
TaggedValue::OptionalDVec2(x) => Box::new(x),
TaggedValue::DAffine2(x) => Box::new(x),
TaggedValue::Image(x) => Box::new(x),
TaggedValue::RcImage(x) => Box::new(x),
TaggedValue::ImaginateCache(x) => Box::new(x),
TaggedValue::ImageFrame(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),
TaggedValue::Subpaths(x) => Box::new(x),
@@ -155,7 +155,7 @@ impl<'a> TaggedValue {
TaggedValue::LuminanceCalculation(x) => Box::new(x),
TaggedValue::ImaginateSamplingMethod(x) => Box::new(x),
TaggedValue::ImaginateMaskStartingFill(x) => Box::new(x),
TaggedValue::ImaginateStatus(x) => Box::new(x),
TaggedValue::ImaginateController(x) => Box::new(x),
TaggedValue::LayerPath(x) => Box::new(x),
TaggedValue::VectorData(x) => Box::new(x),
TaggedValue::Fill(x) => Box::new(x),
@@ -210,7 +210,7 @@ impl<'a> TaggedValue {
TaggedValue::DVec2(_) => concrete!(DVec2),
TaggedValue::OptionalDVec2(_) => concrete!(Option<DVec2>),
TaggedValue::Image(_) => concrete!(graphene_core::raster::Image<Color>),
TaggedValue::RcImage(_) => concrete!(Option<Arc<graphene_core::raster::Image<Color>>>),
TaggedValue::ImaginateCache(_) => concrete!(ImaginateCache),
TaggedValue::ImageFrame(_) => concrete!(graphene_core::raster::ImageFrame<Color>),
TaggedValue::Color(_) => concrete!(graphene_core::raster::Color),
TaggedValue::Subpaths(_) => concrete!(Vec<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>),
@@ -218,7 +218,7 @@ impl<'a> TaggedValue {
TaggedValue::BlendMode(_) => concrete!(BlendMode),
TaggedValue::ImaginateSamplingMethod(_) => concrete!(ImaginateSamplingMethod),
TaggedValue::ImaginateMaskStartingFill(_) => concrete!(ImaginateMaskStartingFill),
TaggedValue::ImaginateStatus(_) => concrete!(ImaginateStatus),
TaggedValue::ImaginateController(_) => concrete!(ImaginateController),
TaggedValue::LayerPath(_) => concrete!(Option<Vec<u64>>),
TaggedValue::DAffine2(_) => concrete!(DAffine2),
TaggedValue::LuminanceCalculation(_) => concrete!(LuminanceCalculation),
@@ -263,7 +263,7 @@ impl<'a> TaggedValue {
x if x == TypeId::of::<DVec2>() => Ok(TaggedValue::DVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<Option<DVec2>>() => Ok(TaggedValue::OptionalDVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::Image<Color>>() => Ok(TaggedValue::Image(*downcast(input).unwrap())),
x if x == TypeId::of::<Option<Arc<graphene_core::raster::Image<Color>>>>() => Ok(TaggedValue::RcImage(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateCache>() => Ok(TaggedValue::ImaginateCache(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::ImageFrame<Color>>() => Ok(TaggedValue::ImageFrame(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::Color>() => Ok(TaggedValue::Color(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>>() => Ok(TaggedValue::Subpaths(*downcast(input).unwrap())),
@@ -271,7 +271,7 @@ impl<'a> TaggedValue {
x if x == TypeId::of::<BlendMode>() => Ok(TaggedValue::BlendMode(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateSamplingMethod>() => Ok(TaggedValue::ImaginateSamplingMethod(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateMaskStartingFill>() => Ok(TaggedValue::ImaginateMaskStartingFill(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateStatus>() => Ok(TaggedValue::ImaginateStatus(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateController>() => Ok(TaggedValue::ImaginateController(*downcast(input).unwrap())),
x if x == TypeId::of::<Option<Vec<u64>>>() => Ok(TaggedValue::LayerPath(*downcast(input).unwrap())),
x if x == TypeId::of::<DAffine2>() => Ok(TaggedValue::DAffine2(*downcast(input).unwrap())),
x if x == TypeId::of::<LuminanceCalculation>() => Ok(TaggedValue::LuminanceCalculation(*downcast(input).unwrap())),

View File

@@ -1,50 +1,155 @@
use dyn_any::{DynAny, StaticType};
use glam::DVec2;
use graphene_core::Color;
use std::borrow::Cow;
use std::fmt::Debug;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
#[derive(Default, Debug, Clone, Copy, PartialEq, DynAny, specta::Type)]
#[derive(Default, Debug, Clone, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateCache(Arc<Mutex<graphene_core::raster::Image<Color>>>);
impl ImaginateCache {
pub fn into_inner(self) -> Arc<Mutex<graphene_core::raster::Image<Color>>> {
self.0
}
}
impl std::cmp::PartialEq for ImaginateCache {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl core::hash::Hash for ImaginateCache {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.lock().unwrap().hash(state);
}
}
pub trait ImaginateTerminationHandle: Debug + Send + Sync + 'static {
fn terminate(&self);
}
#[derive(Default, Debug, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct InternalImaginateControl {
status: Mutex<ImaginateStatus>,
trigger_regenerate: AtomicBool,
#[serde(skip)]
termination_sender: Mutex<Option<Box<dyn ImaginateTerminationHandle>>>,
}
#[derive(Debug, Default, Clone, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateController(Arc<InternalImaginateControl>);
impl ImaginateController {
pub fn get_status(&self) -> ImaginateStatus {
self.0.status.lock().as_deref().cloned().unwrap_or_default()
}
pub fn set_status(&self, status: ImaginateStatus) {
if let Ok(mut lock) = self.0.status.lock() {
*lock = status
}
}
pub fn take_regenerate_trigger(&self) -> bool {
self.0.trigger_regenerate.swap(false, Ordering::SeqCst)
}
pub fn trigger_regenerate(&self) {
self.0.trigger_regenerate.store(true, Ordering::SeqCst)
}
pub fn request_termination(&self) {
if let Some(handle) = self.0.termination_sender.lock().ok().and_then(|mut lock| lock.take()) {
handle.terminate()
}
}
pub fn set_termination_handle<H: ImaginateTerminationHandle>(&self, handle: Box<H>) {
if let Ok(mut lock) = self.0.termination_sender.lock() {
*lock = Some(handle)
}
}
}
impl std::cmp::PartialEq for ImaginateController {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl core::hash::Hash for ImaginateController {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::ptr::hash(Arc::as_ptr(&self.0), state)
}
}
#[derive(Default, Debug, Clone, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ImaginateStatus {
#[default]
Idle,
Ready,
ReadyDone,
Beginning,
Uploading(f64),
Generating,
Uploading,
Generating(f64),
Terminating,
Terminated,
Failed(String),
}
impl ImaginateStatus {
pub fn to_text(&self) -> Cow<'static, str> {
match self {
Self::Ready => Cow::Borrowed("Ready"),
Self::ReadyDone => Cow::Borrowed("Done"),
Self::Beginning => Cow::Borrowed("Beginning…"),
Self::Uploading => Cow::Borrowed("Downloading Image…"),
Self::Generating(percent) => Cow::Owned(format!("Generating {percent:.0}%")),
Self::Terminating => Cow::Owned(format!("Terminating…")),
Self::Terminated => Cow::Owned(format!("Terminated")),
Self::Failed(err) => Cow::Owned(format!("Failed: {err}")),
}
}
}
#[allow(clippy::derived_hash_with_manual_eq)]
impl core::hash::Hash for ImaginateStatus {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::Idle => 0.hash(state),
Self::Beginning => 1.hash(state),
Self::Uploading(f) => {
2.hash(state);
f.to_bits().hash(state);
}
Self::Generating => 3.hash(state),
Self::Terminating => 4.hash(state),
Self::Terminated => 5.hash(state),
Self::Ready | Self::ReadyDone | Self::Beginning | Self::Uploading | Self::Terminating | Self::Terminated => (),
Self::Generating(f) => f.to_bits().hash(state),
Self::Failed(err) => err.hash(state),
}
}
}
#[derive(Debug, Clone, PartialEq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateBaseImage {
pub mime: String,
#[cfg_attr(feature = "serde", serde(rename = "imageData"))]
pub image_data: Vec<u8>,
pub size: DVec2,
#[derive(PartialEq, Eq, Clone, Default, Debug)]
pub enum ImaginateServerStatus {
#[default]
Unknown,
Checking,
Connected,
Failed(String),
Unavailable,
}
#[derive(Debug, Clone, PartialEq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateMaskImage {
pub svg: String,
pub size: DVec2,
impl ImaginateServerStatus {
pub fn to_text(&self) -> Cow<'static, str> {
match self {
Self::Unknown | Self::Checking => Cow::Borrowed("Checking..."),
Self::Connected => Cow::Borrowed("Connected"),
Self::Failed(err) => Cow::Owned(err.clone()),
Self::Unavailable => Cow::Borrowed("Unavailable"),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -180,24 +285,26 @@ impl std::fmt::Display for ImaginateSamplingMethod {
}
}
#[derive(Debug, Clone, PartialEq, specta::Type)]
#[derive(Clone, Debug, PartialEq, Hash, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateGenerationParameters {
pub seed: u64,
pub samples: u32,
/// Use `ImaginateSamplingMethod::api_value()` to generate this string
#[cfg_attr(feature = "serde", serde(rename = "samplingMethod"))]
pub sampling_method: String,
#[cfg_attr(feature = "serde", serde(rename = "denoisingStrength"))]
pub image_creativity: Option<f64>,
#[cfg_attr(feature = "serde", serde(rename = "cfgScale"))]
pub text_guidance: f64,
#[cfg_attr(feature = "serde", serde(rename = "prompt"))]
pub text_prompt: String,
#[cfg_attr(feature = "serde", serde(rename = "negativePrompt"))]
pub negative_prompt: String,
pub resolution: (u32, u32),
#[cfg_attr(feature = "serde", serde(rename = "restoreFaces"))]
pub restore_faces: bool,
pub tiling: bool,
pub struct ImaginatePreferences {
pub host_name: String,
}
impl graphene_core::application_io::GetImaginatePreferences for ImaginatePreferences {
fn get_host_name(&self) -> &str {
&self.host_name
}
}
impl Default for ImaginatePreferences {
fn default() -> Self {
Self {
host_name: "http://localhost:7860/".into(),
}
}
}
unsafe impl dyn_any::StaticType for ImaginatePreferences {
type Static = ImaginatePreferences;
}

View File

@@ -9,12 +9,18 @@ license = "MIT OR Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["wasm"]
gpu = ["graphene-core/gpu", "gpu-compiler-bin-wrapper", "compilation-client", "gpu-executor"]
default = ["wasm", "imaginate"]
gpu = [
"graphene-core/gpu",
"gpu-compiler-bin-wrapper",
"compilation-client",
"gpu-executor",
]
vulkan = ["gpu", "vulkan-executor"]
wgpu = ["gpu", "wgpu-executor"]
quantization = ["autoquant"]
wasm = ["wasm-bindgen", "web-sys", "js-sys"]
imaginate = ["image/png", "base64", "js-sys", "web-sys", "wasm-bindgen-futures"]
[dependencies]
@@ -37,6 +43,7 @@ compilation-client = { path = "../compilation-client", optional = true }
bytemuck = { version = "1.8" }
tempfile = "3"
image = { version = "*", default-features = false }
base64 = { version = "0.21", optional = true }
dyn-clone = "1.0"
log = "0.4"
@@ -48,12 +55,13 @@ glam = { version = "0.22", features = ["serde"] }
node-macro = { path = "../node-macro" }
xxhash-rust = { workspace = true }
serde_json = "1.0.96"
reqwest = { version = "0.11.17", features = ["rustls", "rustls-tls"] }
reqwest = { version = "0.11.18", features = ["rustls", "rustls-tls", "json"] }
futures = "0.3.28"
wasm-bindgen = { version = "0.2.84", optional = true }
js-sys = { version = "0.3.55", optional = true }
js-sys = { version = "0.3.63", optional = true }
wgpu-types = "0.16.0"
wgpu = "0.16.1"
wasm-bindgen-futures = { version = "0.4.36", optional = true }
[dependencies.serde]
version = "1.0"
@@ -62,7 +70,7 @@ features = ["derive"]
[dependencies.web-sys]
version = "0.3.4"
version = "0.3.63"
optional = true
features = [
"Window",

View File

@@ -76,6 +76,7 @@ impl<_I, _O, S0> DynAnyRefNode<_I, _O, S0> {
Self { node, _i: core::marker::PhantomData }
}
}
pub struct DynAnyInRefNode<I, O, Node> {
node: Node,
_i: PhantomData<(I, O)>,
@@ -115,6 +116,10 @@ where
fn reset(&self) {
self.node.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
self.node.serialize()
}
}
impl<N> FutureWrapperNode<N> {

View File

@@ -1,5 +1,3 @@
use std::future::Future;
use crate::Node;
pub struct GetNode;
@@ -17,16 +15,3 @@ pub struct PostNode<Body> {
async fn post_node(url: String, body: String) -> reqwest::Response {
reqwest::Client::new().post(url).body(body).send().await.unwrap()
}
#[derive(Clone, Copy, Debug)]
pub struct EvalSyncNode {}
#[node_macro::node_fn(EvalSyncNode)]
fn eval_sync<F: Future + 'input>(future: F) -> F::Output {
let future = futures::future::maybe_done(future);
futures::pin_mut!(future);
match future.as_mut().take_output() {
Some(value) => value,
_ => panic!("Node construction future returned pending"),
}
}

View File

@@ -0,0 +1,517 @@
use crate::wasm_application_io::WasmEditorApi;
use core::any::TypeId;
use core::future::Future;
use futures::{future::Either, TryFutureExt};
use glam::DVec2;
use graph_craft::imaginate_input::{ImaginateController, ImaginateMaskStartingFill, ImaginatePreferences, ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus, ImaginateTerminationHandle};
use graphene_core::application_io::NodeGraphUpdateMessage;
use graphene_core::raster::{Color, Image, Luma, Pixel};
use image::{DynamicImage, ImageBuffer, ImageOutputFormat};
use reqwest::Url;
const PROGRESS_EVERY_N_STEPS: u32 = 5;
const SDAPI_TEXT_TO_IMAGE: &str = "sdapi/v1/txt2img";
const SDAPI_IMAGE_TO_IMAGE: &str = "sdapi/v1/img2img";
const SDAPI_PROGRESS: &str = "sdapi/v1/progress?skip_current_image=true";
const SDAPI_TERMINATE: &str = "sdapi/v1/interrupt";
fn new_client() -> Result<reqwest::Client, Error> {
reqwest::ClientBuilder::new().build().map_err(Error::ClientBuild)
}
fn parse_url(url: &str) -> Result<Url, Error> {
url.try_into().map_err(|err| Error::UrlParse { text: url.into(), err })
}
fn join_url(base_url: &Url, path: &str) -> Result<Url, Error> {
base_url.join(path).map_err(|err| Error::UrlParse { text: base_url.to_string(), err })
}
fn new_get_request<U: reqwest::IntoUrl>(client: &reqwest::Client, url: U) -> Result<reqwest::Request, Error> {
client.get(url).header("Accept", "*/*").build().map_err(Error::RequestBuild)
}
pub struct ImaginatePersistentData {
pending_server_check: Option<futures::channel::oneshot::Receiver<reqwest::Result<reqwest::Response>>>,
host_name: Url,
client: Option<reqwest::Client>,
server_status: ImaginateServerStatus,
}
impl core::fmt::Debug for ImaginatePersistentData {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct(core::any::type_name::<Self>())
.field("pending_server_check", &self.pending_server_check.is_some())
.field("host_name", &self.host_name)
.field("status", &self.server_status)
.finish()
}
}
impl Default for ImaginatePersistentData {
fn default() -> Self {
let mut status = ImaginateServerStatus::default();
let client = new_client().map_err(|err| status = ImaginateServerStatus::Failed(err.to_string())).ok();
let ImaginatePreferences { host_name } = Default::default();
Self {
pending_server_check: None,
host_name: parse_url(&host_name).unwrap(),
client,
server_status: status,
}
}
}
impl ImaginatePersistentData {
pub fn set_host_name(&mut self, name: &str) {
match parse_url(name) {
Ok(url) => self.host_name = url,
Err(err) => self.server_status = ImaginateServerStatus::Failed(err.to_string()),
}
}
fn initiate_server_check_maybe_fail(&mut self) -> Result<Option<core::pin::Pin<Box<dyn Future<Output = ()> + 'static>>>, Error> {
use futures::future::FutureExt;
let Some(client) = &self.client else { return Ok(None); };
if self.pending_server_check.is_some() {
return Ok(None);
}
self.server_status = ImaginateServerStatus::Checking;
let url = join_url(&self.host_name, SDAPI_PROGRESS)?;
let request = new_get_request(client, url)?;
let (send, recv) = futures::channel::oneshot::channel();
let response_future = client.execute(request).map(move |r| {
let _ = send.send(r);
});
self.pending_server_check = Some(recv);
Ok(Some(Box::pin(response_future)))
}
pub fn initiate_server_check(&mut self) -> Option<core::pin::Pin<Box<dyn Future<Output = ()> + 'static>>> {
match self.initiate_server_check_maybe_fail() {
Ok(f) => f,
Err(err) => {
self.server_status = ImaginateServerStatus::Failed(err.to_string());
None
}
}
}
pub fn poll_server_check(&mut self) {
if let Some(mut check) = self.pending_server_check.take() {
self.server_status = match check.try_recv().map(|r| r.map(|r| r.and_then(reqwest::Response::error_for_status))) {
Ok(Some(Ok(_response))) => ImaginateServerStatus::Connected,
Ok(Some(Err(_))) | Err(_) => ImaginateServerStatus::Unavailable,
Ok(None) => {
self.pending_server_check = Some(check);
ImaginateServerStatus::Checking
}
}
}
}
pub fn server_status(&self) -> &ImaginateServerStatus {
&self.server_status
}
pub fn is_checking(&self) -> bool {
matches!(self.server_status, ImaginateServerStatus::Checking)
}
}
#[derive(Debug)]
struct ImaginateFutureAbortHandle(futures::future::AbortHandle);
impl ImaginateTerminationHandle for ImaginateFutureAbortHandle {
fn terminate(&self) {
self.0.abort()
}
}
#[derive(Debug)]
enum Error {
UrlParse { text: String, err: <&'static str as TryInto<Url>>::Error },
ClientBuild(reqwest::Error),
RequestBuild(reqwest::Error),
Request(reqwest::Error),
ResponseFormat(reqwest::Error),
NoImage,
Base64Decode(base64::DecodeError),
ImageDecode(image::error::ImageError),
ImageEncode(image::error::ImageError),
UnsupportedPixelType(&'static str),
InconsistentImageSize,
Terminated,
TerminationFailed(reqwest::Error),
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
Self::UrlParse { text, err } => write!(f, "invalid url '{text}' ({err})"),
Self::ClientBuild(err) => write!(f, "failed to create a reqwest client ({err})"),
Self::RequestBuild(err) => write!(f, "failed to create a reqwest request ({err})"),
Self::Request(err) => write!(f, "request failed ({err})"),
Self::ResponseFormat(err) => write!(f, "got an invalid API response ({err})"),
Self::NoImage => write!(f, "got an empty API response"),
Self::Base64Decode(err) => write!(f, "failed to decode base64 encoded image ({err})"),
Self::ImageDecode(err) => write!(f, "failed to decode png image ({err})"),
Self::ImageEncode(err) => write!(f, "failed to encode png image ({err})"),
Self::UnsupportedPixelType(ty) => write!(f, "pixel type `{ty}` not supported for imaginate images"),
Self::InconsistentImageSize => write!(f, "image width and height do not match the image byte size"),
Self::Terminated => write!(f, "imaginate request was terminated by the user"),
Self::TerminationFailed(err) => write!(f, "termination failed ({err})"),
}
}
}
impl std::error::Error for Error {}
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
struct ImageResponse {
images: Vec<String>,
}
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
struct ProgressResponse {
progress: f64,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct ImaginateTextToImageRequestOverrideSettings {
show_progress_every_n_steps: u32,
}
impl Default for ImaginateTextToImageRequestOverrideSettings {
fn default() -> Self {
Self {
show_progress_every_n_steps: PROGRESS_EVERY_N_STEPS,
}
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct ImaginateImageToImageRequestOverrideSettings {
show_progress_every_n_steps: u32,
img2img_fix_steps: bool,
}
impl Default for ImaginateImageToImageRequestOverrideSettings {
fn default() -> Self {
Self {
show_progress_every_n_steps: PROGRESS_EVERY_N_STEPS,
img2img_fix_steps: true,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct ImaginateTextToImageRequest<'a> {
#[serde(flatten)]
common: ImaginateCommonImageRequest<'a>,
override_settings: ImaginateTextToImageRequestOverrideSettings,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct ImaginateMask {
mask: String,
mask_blur: String,
inpainting_fill: u32,
inpaint_full_res: bool,
inpainting_mask_invert: u32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct ImaginateImageToImageRequest<'a> {
#[serde(flatten)]
common: ImaginateCommonImageRequest<'a>,
override_settings: ImaginateImageToImageRequestOverrideSettings,
init_images: Vec<String>,
denoising_strength: f64,
#[serde(flatten)]
mask: Option<ImaginateMask>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct ImaginateCommonImageRequest<'a> {
prompt: String,
seed: f64,
steps: u32,
cfg_scale: f64,
width: f64,
height: f64,
restore_faces: bool,
tiling: bool,
negative_prompt: String,
sampler_index: &'a str,
}
#[cfg(feature = "imaginate")]
pub async fn imaginate<'a, P: Pixel>(
image: Image<P>,
editor_api: impl Future<Output = WasmEditorApi<'a>>,
controller: ImaginateController,
seed: impl Future<Output = f64>,
res: impl Future<Output = Option<DVec2>>,
samples: impl Future<Output = u32>,
sampling_method: impl Future<Output = ImaginateSamplingMethod>,
prompt_guidance: impl Future<Output = f64>,
prompt: impl Future<Output = String>,
negative_prompt: impl Future<Output = String>,
adapt_input_image: impl Future<Output = bool>,
image_creativity: impl Future<Output = f64>,
masking_layer: impl Future<Output = Option<Vec<u64>>>,
inpaint: impl Future<Output = bool>,
mask_blur: impl Future<Output = f64>,
mask_starting_fill: impl Future<Output = ImaginateMaskStartingFill>,
improve_faces: impl Future<Output = bool>,
tiling: impl Future<Output = bool>,
) -> Image<P> {
let WasmEditorApi {
node_graph_message_sender,
imaginate_preferences,
..
} = editor_api.await;
let set_progress = |progress: ImaginateStatus| {
controller.set_status(progress);
node_graph_message_sender.send(NodeGraphUpdateMessage::ImaginateStatusUpdate);
};
let host_name = imaginate_preferences.get_host_name();
imaginate_maybe_fail(
image,
host_name,
set_progress,
&controller,
seed,
res,
samples,
sampling_method,
prompt_guidance,
prompt,
negative_prompt,
adapt_input_image,
image_creativity,
masking_layer,
inpaint,
mask_blur,
mask_starting_fill,
improve_faces,
tiling,
)
.await
.unwrap_or_else(|err| {
match err {
Error::Terminated => {
set_progress(ImaginateStatus::Terminated);
}
err => {
error!("{err}");
set_progress(ImaginateStatus::Failed(err.to_string()));
}
};
Image::empty()
})
}
#[cfg(feature = "imaginate")]
async fn imaginate_maybe_fail<'a, P: Pixel, F: Fn(ImaginateStatus)>(
image: Image<P>,
host_name: &str,
set_progress: F,
controller: &ImaginateController,
seed: impl Future<Output = f64>,
res: impl Future<Output = Option<DVec2>>,
samples: impl Future<Output = u32>,
sampling_method: impl Future<Output = ImaginateSamplingMethod>,
prompt_guidance: impl Future<Output = f64>,
prompt: impl Future<Output = String>,
negative_prompt: impl Future<Output = String>,
adapt_input_image: impl Future<Output = bool>,
image_creativity: impl Future<Output = f64>,
_masking_layer: impl Future<Output = Option<Vec<u64>>>,
_inpaint: impl Future<Output = bool>,
_mask_blur: impl Future<Output = f64>,
_mask_starting_fill: impl Future<Output = ImaginateMaskStartingFill>,
improve_faces: impl Future<Output = bool>,
tiling: impl Future<Output = bool>,
) -> Result<Image<P>, Error> {
set_progress(ImaginateStatus::Beginning);
let base_url: Url = parse_url(host_name)?;
let client = new_client()?;
let sampler_index = sampling_method.await;
let sampler_index = sampler_index.api_value();
let res = res.await.unwrap_or_else(|| {
let (width, height) = pick_safe_imaginate_resolution((image.width as _, image.height as _));
DVec2::new(width as _, height as _)
});
let common_request_data = ImaginateCommonImageRequest {
prompt: prompt.await,
seed: seed.await,
steps: samples.await,
cfg_scale: prompt_guidance.await,
width: res.x,
height: res.y,
restore_faces: improve_faces.await,
tiling: tiling.await,
negative_prompt: negative_prompt.await,
sampler_index,
};
let request_builder = if adapt_input_image.await {
let base64_data = image_to_base64(image)?;
let request_data = ImaginateImageToImageRequest {
common: common_request_data,
override_settings: Default::default(),
init_images: vec![base64_data],
denoising_strength: image_creativity.await * 0.01,
mask: None,
};
let url = join_url(&base_url, SDAPI_IMAGE_TO_IMAGE)?;
client.post(url).json(&request_data)
} else {
let request_data = ImaginateTextToImageRequest {
common: common_request_data,
override_settings: Default::default(),
};
let url = join_url(&base_url, SDAPI_TEXT_TO_IMAGE)?;
client.post(url).json(&request_data)
};
let request = request_builder.header("Accept", "*/*").build().map_err(Error::RequestBuild)?;
let (response_future, abort_handle) = futures::future::abortable(client.execute(request));
controller.set_termination_handle(Box::new(ImaginateFutureAbortHandle(abort_handle)));
let progress_url = join_url(&base_url, SDAPI_PROGRESS)?;
futures::pin_mut!(response_future);
let response = loop {
let progress_request = new_get_request(&client, progress_url.clone())?;
let progress_response_future = client.execute(progress_request).and_then(|response| response.json());
futures::pin_mut!(progress_response_future);
response_future = match futures::future::select(response_future, progress_response_future).await {
Either::Left((response, _)) => break response,
Either::Right((progress, response_future)) => {
if let Ok(ProgressResponse { progress }) = progress {
set_progress(ImaginateStatus::Generating(progress * 100.));
}
response_future
}
};
};
let response = match response {
Ok(response) => response.and_then(reqwest::Response::error_for_status).map_err(Error::Request)?,
Err(_aborted) => {
set_progress(ImaginateStatus::Terminating);
let url = join_url(&base_url, SDAPI_TERMINATE)?;
let request = client.post(url).build().map_err(Error::RequestBuild)?;
// The user probably doesn't really care if the server side was really aborted or if there was an network error.
// So we fool them that the request was terminated if the termination request in reality failed.
let _ = client.execute(request).await.and_then(reqwest::Response::error_for_status).map_err(Error::TerminationFailed)?;
return Err(Error::Terminated);
}
};
set_progress(ImaginateStatus::Uploading);
let ImageResponse { images } = response.json().await.map_err(Error::ResponseFormat)?;
let result = images.into_iter().next().ok_or(Error::NoImage).and_then(base64_to_image)?;
set_progress(ImaginateStatus::ReadyDone);
Ok(result)
}
fn image_to_base64<P: Pixel>(image: Image<P>) -> Result<String, Error> {
use base64::prelude::*;
let Image { width, height, data } = image;
fn cast_with_f32<S: Pixel, D: image::Pixel<Subpixel = f32>>(data: Vec<S>, width: u32, height: u32) -> Result<DynamicImage, Error>
where
DynamicImage: From<ImageBuffer<D, Vec<f32>>>,
{
ImageBuffer::<D, Vec<f32>>::from_raw(width, height, bytemuck::cast_vec(data))
.ok_or(Error::InconsistentImageSize)
.map(Into::into)
}
let image: DynamicImage = match TypeId::of::<P>() {
id if id == TypeId::of::<Color>() => cast_with_f32::<_, image::Rgba<f32>>(data, width, height)?
// we need to do this cast, because png does not support rgba32f
.to_rgba16().into(),
id if id == TypeId::of::<Luma>() => cast_with_f32::<_, image::Luma<f32>>(data, width, height)?
// we need to do this cast, because png does not support luma32f
.to_luma16().into(),
_ => return Err(Error::UnsupportedPixelType(core::any::type_name::<P>())),
};
let mut png_data = std::io::Cursor::new(vec![]);
image.write_to(&mut png_data, ImageOutputFormat::Png).map_err(Error::ImageEncode)?;
Ok(BASE64_STANDARD.encode(png_data.into_inner()))
}
fn base64_to_image<D: AsRef<[u8]>, P: Pixel>(base64_data: D) -> Result<Image<P>, Error> {
use base64::prelude::*;
let png_data = BASE64_STANDARD.decode(base64_data).map_err(Error::Base64Decode)?;
let dyn_image = image::load_from_memory_with_format(&png_data, image::ImageFormat::Png).map_err(Error::ImageDecode)?;
let (width, height) = (dyn_image.width(), dyn_image.height());
let result_data: Vec<P> = match TypeId::of::<P>() {
id if id == TypeId::of::<Color>() => bytemuck::cast_vec(dyn_image.into_rgba32f().into_raw()),
id if id == TypeId::of::<Luma>() => bytemuck::cast_vec(dyn_image.to_luma32f().into_raw()),
_ => return Err(Error::UnsupportedPixelType(core::any::type_name::<P>())),
};
Ok(Image { data: result_data, width, height })
}
pub fn pick_safe_imaginate_resolution((width, height): (f64, f64)) -> (u64, u64) {
const MAX_RESOLUTION: u64 = 1000 * 1000;
// this is the maximum width/height that can be obtained
const MAX_DIMENSION: u64 = (MAX_RESOLUTION / 64) & !63;
// round the resolution to the nearest multiple of 64
let [width, height] = [width, height].map(|c| (c.round().clamp(0., MAX_DIMENSION as _) as u64 + 32).max(64) & !63);
let resolution = width * height;
if resolution > MAX_RESOLUTION {
// scale down the image, so it is smaller than MAX_RESOLUTION
let scale = (MAX_RESOLUTION as f64 / resolution as f64).sqrt();
let [width, height] = [width, height].map(|c| c as f64 * scale);
if width < 64.0 {
// the image is extremely wide
(64, MAX_DIMENSION)
} else if height < 64.0 {
// the image is extremely high
(MAX_DIMENSION, 64)
} else {
// round down to a multiple of 64, so that the resolution still is smaller than MAX_RESOLUTION
let [width, height] = [width, height].map(|c| c as u64 & !63);
(width, height)
}
} else {
(width, height)
}
}

View File

@@ -25,3 +25,5 @@ pub mod brush;
#[cfg(feature = "wasm")]
pub mod wasm_application_io;
pub mod imaginate;

View File

@@ -1,8 +1,11 @@
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use graph_craft::imaginate_input::{ImaginateController, ImaginateMaskStartingFill, ImaginateSamplingMethod};
use graph_craft::proto::DynFuture;
use graphene_core::raster::{Alpha, BlendMode, BlendNode, Image, ImageFrame, Linear, LinearChannel, Luminance, Pixel, RGBMut, Raster, RasterMut, RedGreenBlue, Sample};
use graphene_core::transform::Transform;
use crate::wasm_application_io::WasmEditorApi;
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::value::CopiedNode;
use graphene_core::{Color, Node};
@@ -414,19 +417,74 @@ fn empty_image<_P: Pixel>(transform: DAffine2, color: _P) -> ImageFrame<_P> {
ImageFrame { image, transform }
}
#[derive(Debug, Clone, Copy)]
pub struct ImaginateNode<P, E> {
cached: E,
_p: PhantomData<P>,
macro_rules! generate_imaginate_node {
($($val:ident: $t:ident: $o:ty,)*) => {
pub struct ImaginateNode<P: Pixel, E, C, $($t,)*> {
editor_api: E,
controller: C,
$($val: $t,)*
cache: std::sync::Arc<std::sync::Mutex<Image<P>>>,
}
impl<'e, P: Pixel, E, C, $($t,)*> ImaginateNode<P, E, C, $($t,)*>
where $($t: for<'any_input> Node<'any_input, (), Output = DynFuture<'any_input, $o>>,)*
E: for<'any_input> Node<'any_input, (), Output = DynFuture<'any_input, WasmEditorApi<'e>>>,
C: for<'any_input> Node<'any_input, (), Output = DynFuture<'any_input, ImaginateController>>,
{
pub fn new(editor_api: E, controller: C, $($val: $t,)* cache: std::sync::Arc<std::sync::Mutex<Image<P>>>) -> Self {
Self { editor_api, controller, $($val,)* cache }
}
}
impl<'i, 'e: 'i, P: Pixel + 'i, E: 'i, C: 'i, $($t: 'i,)*> Node<'i, ImageFrame<P>> for ImaginateNode<P, E, C, $($t,)*>
where $($t: for<'any_input> Node<'any_input, (), Output = DynFuture<'any_input, $o>>,)*
E: for<'any_input> Node<'any_input, (), Output = DynFuture<'any_input, WasmEditorApi<'e>>>,
C: for<'any_input> Node<'any_input, (), Output = DynFuture<'any_input, ImaginateController>>,
{
type Output = DynFuture<'i, ImageFrame<P>>;
fn eval(&'i self, frame: ImageFrame<P>) -> Self::Output {
let controller = self.controller.eval(());
$(let $val = self.$val.eval(());)*
Box::pin(async move {
let controller: std::pin::Pin<Box<dyn std::future::Future<Output = ImaginateController>>> = controller;
let controller: ImaginateController = controller.await;
if controller.take_regenerate_trigger() {
let editor_api = self.editor_api.eval(());
let image = super::imaginate::imaginate(frame.image, editor_api, controller, $($val,)*).await;
self.cache.lock().unwrap().clone_from(&image);
return ImageFrame {
image,
..frame
}
}
let image = self.cache.lock().unwrap().clone();
ImageFrame {
image,
..frame
}
})
}
}
}
}
#[node_macro::node_fn(ImaginateNode<_P>)]
fn imaginate<_P: Pixel>(image_frame: ImageFrame<_P>, cached: Option<std::sync::Arc<graphene_core::raster::Image<_P>>>) -> ImageFrame<_P> {
let cached_image = cached.map(|mut x| std::sync::Arc::make_mut(&mut x).clone()).unwrap_or(image_frame.image);
ImageFrame {
image: cached_image,
transform: image_frame.transform,
}
generate_imaginate_node! {
seed: Seed: f64,
res: Res: Option<DVec2>,
samples: Samples: u32,
sampling_method: SamplingMethod: ImaginateSamplingMethod,
prompt_guidance: PromptGuidance: f64,
prompt: Prompt: String,
negative_prompt: NegativePrompt: String,
adapt_input_image: AdaptInputImage: bool,
image_creativity: ImageCreativity: f64,
masking_layer: MaskingLayer: Option<Vec<u64>>,
inpaint: Inpaint: bool,
mask_blur: MaskBlur: f64,
mask_starting_fill: MaskStartingFill: ImaginateMaskStartingFill,
improve_faces: ImproveFaces: bool,
tiling: Tiling: bool,
}
#[derive(Debug, Clone, Copy)]

View File

@@ -3,7 +3,6 @@ pub mod node_registry;
#[cfg(test)]
mod tests {
use graph_craft::document::value::TaggedValue;
use graphene_core::*;
use std::borrow::Cow;

View File

@@ -1,3 +1,4 @@
use graph_craft::imaginate_input::{ImaginateCache, ImaginateController, ImaginateMaskStartingFill, ImaginateSamplingMethod};
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_core::ops::IdNode;
use graphene_core::quantization::QuantizationChannels;
@@ -444,18 +445,59 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
params: [WasmSurfaceHandleFrame]
),
async_node!(graphene_core::memo::EndLetNode<_>, input: WasmEditorApi, output: SurfaceFrame, params: [SurfaceFrame]),
vec![(
NodeIdentifier::new("graphene_core::memo::RefNode<_, _>"),
|args| {
Box::pin(async move {
let node: DowncastBothNode<Option<WasmEditorApi>, WasmEditorApi> = graphene_std::any::DowncastBothNode::new(args[0].clone());
let node = <graphene_core::memo::RefNode<_, _>>::new(node);
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node));
any.into_type_erased()
})
},
NodeIOTypes::new(concrete!(()), concrete!(WasmEditorApi), vec![fn_type!(Option<WasmEditorApi>, WasmEditorApi)]),
)],
vec![
(
NodeIdentifier::new("graphene_core::memo::RefNode<_, _>"),
|args| {
Box::pin(async move {
let node: DowncastBothNode<Option<WasmEditorApi>, WasmEditorApi> = graphene_std::any::DowncastBothNode::new(args[0].clone());
let node = <graphene_core::memo::RefNode<_, _>>::new(node);
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node));
any.into_type_erased()
})
},
NodeIOTypes::new(concrete!(()), concrete!(WasmEditorApi), vec![fn_type!(Option<WasmEditorApi>, WasmEditorApi)]),
),
(
NodeIdentifier::new("graphene_std::raster::ImaginateNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _>"),
|args: Vec<Arc<graph_craft::proto::NodeContainer>>| {
Box::pin(async move {
use graphene_std::raster::ImaginateNode;
let cache: ImaginateCache = graphene_std::any::input_node(args.last().unwrap().clone()).eval(()).await;
macro_rules! instanciate_imaginate_node {
($($i:expr,)*) => { ImaginateNode::new($(graphene_std::any::input_node(args[$i].clone()),)* cache.into_inner()) };
}
let node: ImaginateNode<Color, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _> = instanciate_imaginate_node!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,);
let any = graphene_std::any::DynAnyNode::new(ValueNode::new(node));
any.into_type_erased()
})
},
NodeIOTypes::new(
concrete!(ImageFrame<Color>),
concrete!(ImageFrame<Color>),
vec![
fn_type!(WasmEditorApi),
fn_type!(ImaginateController),
fn_type!(f64),
fn_type!(Option<DVec2>),
fn_type!(u32),
fn_type!(ImaginateSamplingMethod),
fn_type!(f64),
fn_type!(String),
fn_type!(String),
fn_type!(bool),
fn_type!(f64),
fn_type!(Option<Vec<u64>>),
fn_type!(bool),
fn_type!(f64),
fn_type!(ImaginateMaskStartingFill),
fn_type!(bool),
fn_type!(bool),
fn_type!(ImaginateCache),
],
),
),
],
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: Image<Color>, params: [Image<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: ImageFrame<Color>, params: [ImageFrame<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: QuantizationChannels, params: [QuantizationChannels]),