Compare commits

...
Author SHA1 Message Date
Brandon Thomas b6a9d52e19 implement cumprod 2025-02-25 00:25:19 -05:00
Brandon Thomas 97fe9f90c9 more work on timestamps (WIP) 2025-02-24 19:36:31 -05:00
Brandon Thomas 5cec883b15 cleanup 2025-02-24 17:48:24 -05:00
Brandon Thomas 70fa084e30 step() mostly done 2025-02-24 17:45:08 -05:00
Brandon Thomas 8111c84d12 implementing step() 2025-02-24 17:22:25 -05:00
Brandon Thomas a55e3f84dc notes 2025-02-24 02:07:48 -05:00
Brandon Thomas 221861eb9f clarify 2025-02-23 23:08:14 -05:00
Brandon Thomas e9e6c66ba1 add_noise WIP 2025-02-23 22:38:31 -05:00
Brandon Thomas d2da91a8bd implement constructor, tests 2025-02-23 20:53:19 -05:00
Brandon Thomas ee5efc378e implement timestep initialization 2025-02-23 14:56:49 -05:00
Brandon Thomas dbd06ee7b3 LCM implementation (wip) 2025-02-23 14:37:24 -05:00
Michael Chung 6077232cd3 Hide Preview Drag Layer as it is not done. 2025-02-21 17:21:43 -05:00
Michael Chung f013373180 Copy Shift Still needs some UX work but it does work.
Need to store it as a image node.
2025-02-21 00:39:59 -05:00
Michael Chung f0c1fcf516 Got the color changes to work. 2025-02-20 22:34:17 -05:00
Brandon Thomas 9f392de9c5 bump packages to enable windows builds 2025-02-20 18:10:07 -05:00
BFlat f5fb340de9 landing hero update (#419) 2025-02-21 01:49:28 +07:00
Tanish Manku 0392bda03c Forward color picker events to engine (#418) 2025-02-20 11:38:29 -05:00
22 changed files with 1173 additions and 86 deletions
Generated
+219 -46
View File
@@ -609,6 +609,7 @@ dependencies = [
"candle-core",
"candle-nn",
"candle-transformers",
"cudarc",
"filesys",
"hf-hub",
"image",
@@ -1424,14 +1425,14 @@ dependencies = [
[[package]]
name = "candle-core"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "855dfedff437d2681d68e1f34ae559d88b0dd84aa5a6b63f2c8e75ebdd875bbf"
checksum = "bef79e00300acaaa6b0b33168e2ed5050f06125d1add87e7103c40a119083581"
dependencies = [
"byteorder",
"candle-kernels",
"cudarc",
"gemm",
"gemm 0.17.1",
"half",
"memmap2",
"num-traits",
@@ -1449,9 +1450,9 @@ dependencies = [
[[package]]
name = "candle-kernels"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53343628fa470b7075c28c589b98735b4220b464e37ddbb8e117040e199f4787"
checksum = "e3134cc19a080a326f95bdcbe057ecaa9658598e5146e37ba1a8cd550c342a9b"
dependencies = [
"bindgen_cuda",
]
@@ -2228,9 +2229,9 @@ dependencies = [
[[package]]
name = "cudarc"
version = "0.12.1"
version = "0.13.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38cd60a9a42ec83a2ed7effb0b1f073270264ea99da7acfc44f7e8d74dee0384"
checksum = "4e29ce3bfa797c1183053ceb496316203ef561c183941c3c181500d9ade6daf4"
dependencies = [
"half",
"libloading 0.8.6",
@@ -2891,6 +2892,15 @@ dependencies = [
"reborrow",
]
[[package]]
name = "dyn-stack"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd"
dependencies = [
"bytemuck",
]
[[package]]
name = "easyenv"
version = "0.1.0"
@@ -3743,17 +3753,37 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32"
dependencies = [
"dyn-stack",
"gemm-c32",
"gemm-c64",
"gemm-common",
"gemm-f16",
"gemm-f32",
"gemm-f64",
"dyn-stack 0.10.0",
"gemm-c32 0.17.1",
"gemm-c64 0.17.1",
"gemm-common 0.17.1",
"gemm-f16 0.17.1",
"gemm-f32 0.17.1",
"gemm-f64 0.17.1",
"num-complex",
"num-traits",
"paste",
"raw-cpuid",
"raw-cpuid 10.7.0",
"seq-macro",
]
[[package]]
name = "gemm"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451"
dependencies = [
"dyn-stack 0.13.0",
"gemm-c32 0.18.2",
"gemm-c64 0.18.2",
"gemm-common 0.18.2",
"gemm-f16 0.18.2",
"gemm-f32 0.18.2",
"gemm-f64 0.18.2",
"num-complex",
"num-traits",
"paste",
"raw-cpuid 11.4.0",
"seq-macro",
]
@@ -3763,12 +3793,27 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0"
dependencies = [
"dyn-stack",
"gemm-common",
"dyn-stack 0.10.0",
"gemm-common 0.17.1",
"num-complex",
"num-traits",
"paste",
"raw-cpuid",
"raw-cpuid 10.7.0",
"seq-macro",
]
[[package]]
name = "gemm-c32"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847"
dependencies = [
"dyn-stack 0.13.0",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
"paste",
"raw-cpuid 11.4.0",
"seq-macro",
]
@@ -3778,12 +3823,27 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a"
dependencies = [
"dyn-stack",
"gemm-common",
"dyn-stack 0.10.0",
"gemm-common 0.17.1",
"num-complex",
"num-traits",
"paste",
"raw-cpuid",
"raw-cpuid 10.7.0",
"seq-macro",
]
[[package]]
name = "gemm-c64"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf"
dependencies = [
"dyn-stack 0.13.0",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
"paste",
"raw-cpuid 11.4.0",
"seq-macro",
]
@@ -3794,17 +3854,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8"
dependencies = [
"bytemuck",
"dyn-stack",
"dyn-stack 0.10.0",
"half",
"num-complex",
"num-traits",
"once_cell",
"paste",
"pulp",
"raw-cpuid",
"pulp 0.18.22",
"raw-cpuid 10.7.0",
"rayon",
"seq-macro",
"sysctl",
"sysctl 0.5.5",
]
[[package]]
name = "gemm-common"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3"
dependencies = [
"bytemuck",
"dyn-stack 0.13.0",
"half",
"libm",
"num-complex",
"num-traits",
"once_cell",
"paste",
"pulp 0.21.4",
"raw-cpuid 11.4.0",
"rayon",
"seq-macro",
"sysctl 0.6.0",
]
[[package]]
@@ -3813,14 +3894,32 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4"
dependencies = [
"dyn-stack",
"gemm-common",
"gemm-f32",
"dyn-stack 0.10.0",
"gemm-common 0.17.1",
"gemm-f32 0.17.1",
"half",
"num-complex",
"num-traits",
"paste",
"raw-cpuid",
"raw-cpuid 10.7.0",
"rayon",
"seq-macro",
]
[[package]]
name = "gemm-f16"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109"
dependencies = [
"dyn-stack 0.13.0",
"gemm-common 0.18.2",
"gemm-f32 0.18.2",
"half",
"num-complex",
"num-traits",
"paste",
"raw-cpuid 11.4.0",
"rayon",
"seq-macro",
]
@@ -3831,12 +3930,27 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113"
dependencies = [
"dyn-stack",
"gemm-common",
"dyn-stack 0.10.0",
"gemm-common 0.17.1",
"num-complex",
"num-traits",
"paste",
"raw-cpuid",
"raw-cpuid 10.7.0",
"seq-macro",
]
[[package]]
name = "gemm-f32"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864"
dependencies = [
"dyn-stack 0.13.0",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
"paste",
"raw-cpuid 11.4.0",
"seq-macro",
]
@@ -3846,12 +3960,27 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0"
dependencies = [
"dyn-stack",
"gemm-common",
"dyn-stack 0.10.0",
"gemm-common 0.17.1",
"num-complex",
"num-traits",
"paste",
"raw-cpuid",
"raw-cpuid 10.7.0",
"seq-macro",
]
[[package]]
name = "gemm-f64"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd"
dependencies = [
"dyn-stack 0.13.0",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
"paste",
"raw-cpuid 11.4.0",
"seq-macro",
]
@@ -5237,9 +5366,9 @@ dependencies = [
[[package]]
name = "libm"
version = "0.2.8"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libredox"
@@ -6238,7 +6367,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate 3.1.0",
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 2.0.87",
@@ -7323,6 +7452,20 @@ dependencies = [
"reborrow",
]
[[package]]
name = "pulp"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95fb7a99b37aaef4c7dd2fd15a819eb8010bfc7a2c2155230d51f497316cad6d"
dependencies = [
"bytemuck",
"cfg-if 1.0.0",
"libm",
"num-complex",
"reborrow",
"version_check",
]
[[package]]
name = "qoi"
version = "0.4.1"
@@ -7636,6 +7779,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "raw-cpuid"
version = "11.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "529468c1335c1c03919960dfefdb1b3648858c20d7ec2d0663e728e4a717efbc"
dependencies = [
"bitflags 2.5.0",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -9774,6 +9926,20 @@ dependencies = [
"walkdir",
]
[[package]]
name = "sysctl"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc"
dependencies = [
"bitflags 2.5.0",
"byteorder",
"enum-as-inner",
"libc",
"thiserror 1.0.63",
"walkdir",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
@@ -10846,27 +11012,34 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ug"
version = "0.0.2"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4eef2ebfc18c67a6dbcacd9d8a4d85e0568cc58c82515552382312c2730ea13"
checksum = "03719c61a91b51541f076dfdba45caacf750b230cefaa4b32d6f5411c3f7f437"
dependencies = [
"gemm 0.18.2",
"half",
"libloading 0.8.6",
"memmap2",
"num",
"num-traits",
"num_cpus",
"rayon",
"safetensors",
"serde",
"serde_json",
"thiserror 1.0.63",
"tracing",
"yoke",
]
[[package]]
name = "ug-cuda"
version = "0.0.2"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c4dcab280ad0ef3957e153a82dcad608c954d02cf253b695322f502d1f8902e"
checksum = "50758486d7941f8b0a636ba7e29455c07071f41590beac1fd307ec893e8db69a"
dependencies = [
"cudarc",
"half",
"serde",
"serde_json",
"thiserror 1.0.63",
"ug",
]
@@ -11192,9 +11365,9 @@ dependencies = [
[[package]]
name = "version_check"
version = "0.9.4"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "videos"
+10 -1
View File
@@ -23,6 +23,9 @@ tauri-build = { version = "2.0.4", features = [] }
filesys = { path = "../../lib/files/filesys" }
memory_caching = { path = "../../lib/caching/memory_caching" }
# Bypass cudarc issues on Windows https://github.com/coreylowman/cudarc/issues/332
cudarc = "0.13.7"
# External
anyhow = "1.0"
bytes = "1.10"
@@ -44,4 +47,10 @@ tokenizers = "0.15.2"
[features]
default = ["cuda"]
cuda = ["candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda"]
cuda = [
"candle-core/cuda",
# TODO(bt,2025-02-22): cuDNN might speed up Stable Diffusion
# "candle-core/cudnn",
"candle-nn/cuda",
"candle-transformers/cuda",
]
+150
View File
@@ -0,0 +1,150 @@
use candle_core::shape::Dim;
use candle_core::Tensor;
/// Tensor Cumulative Product
///
/// Candle doesn't implement cumprod, only cumsum.
/// This implementation is adapted from cumsum in candle-ext:
/// https://github.com/mokeyish/candle-ext/blob/main/tests/cumsum.rs
///
/// See Also: https://pytorch.org/docs/stable/generated/torch.cumprod.html
/// See Also: https://numpy.org/doc/stable/reference/generated/numpy.cumprod.html
/// See Also: https://github.com/huggingface/candle/issues/1646
///
pub fn cumprod<D: Dim>(input: &Tensor, dim: D) -> candle_core::Result<Tensor> {
let dim = dim.to_index(input.shape(), "cumprod")?;
let dim_size = input.dim(dim)?;
let rank = input.rank();
if rank == 0 {
return Ok(input.clone());
}
let mut tensors = Vec::with_capacity(dim_size);
let mut a = input.clone();
for i in 0..dim_size {
if i > 0 {
a = a.narrow(dim, 1, dim_size - i)?;
let b = input.narrow(dim, 0, dim_size - i)?;
a = (a * b)?;
}
tensors.push(a.narrow(dim, 0, 1)?);
}
let cumsum = Tensor::cat(&tensors, dim)?;
Ok(cumsum)
}
#[cfg(test)]
mod tests {
use crate::ml::cumprod::cumprod;
use candle_core::{Device, Tensor};
mod base_cases {
use super::*;
#[test]
fn zeroes_vector() -> anyhow::Result<()> {
let a = Tensor::new(&[0i64, 0, 0, 0], &Device::Cpu)?;
let b = cumprod(&a, 0)?;
assert_eq!(b.to_vec1::<i64>()?, &[0, 0, 0, 0]);
Ok(())
}
#[test]
fn zeroes_tensor() -> anyhow::Result<()> {
let a = Tensor::new(&[
[0i64, 0, 0],
[0i64, 0, 0],
[0i64, 0, 0],
], &Device::Cpu)?;
let b = cumprod(&a, 0)?;
assert_eq!(b.to_vec2::<i64>()?, &[
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
]);
Ok(())
}
#[test]
fn ones_vector() -> anyhow::Result<()> {
let a = Tensor::new(&[1i64, 1, 1, 1], &Device::Cpu)?;
let b = cumprod(&a, 0)?;
assert_eq!(b.to_vec1::<i64>()?, &[1, 1, 1, 1]);
Ok(())
}
#[test]
fn ones_tensor() -> anyhow::Result<()> {
let a = Tensor::new(&[
[1i64, 1, 1],
[1i64, 1, 1],
[1i64, 1, 1],
], &Device::Cpu)?;
let b = cumprod(&a, 0)?;
assert_eq!(b.to_vec2::<i64>()?, &[
[1, 1, 1],
[1, 1, 1],
[1, 1, 1],
]);
Ok(())
}
}
mod test_cases {
use super::*;
#[test]
fn vector() -> anyhow::Result<()> {
let a = Tensor::new(&[1i64, 2, 3, 4, 5], &Device::Cpu)?;
let b = cumprod(&a, 0)?;
assert_eq!(b.to_vec1::<i64>()?, &[1, 2, 6, 24, 120]);
let a = Tensor::new(&[0, 1i64, 2, 3, 4, 5], &Device::Cpu)?;
let b = cumprod(&a, 0)?;
assert_eq!(b.to_vec1::<i64>()?, &[0, 0, 0, 0, 0, 0]);
Ok(())
}
#[test]
fn tensor_dim_0() -> anyhow::Result<()> {
let a = Tensor::new(&[
[1i64, 2, 3],
[4i64, 5, 6],
[7i64, 8, 9],
], &Device::Cpu)?;
let b = cumprod(&a, 0)?;
assert_eq!(b.to_vec2::<i64>()?, &[
[1, 2, 3],
[4, 10, 18],
[28, 80, 162],
]);
Ok(())
}
#[test]
fn tensor_dim_1() -> anyhow::Result<()> {
let a = Tensor::new(&[
[1i64, 2, 3],
[4i64, 5, 6],
[7i64, 8, 9],
], &Device::Cpu)?;
let b = cumprod(&a, 1)?;
assert_eq!(b.to_vec2::<i64>()?, &[
[1, 2, 6],
[4, 20, 120],
[7, 56, 504],
]);
Ok(())
}
}
}
@@ -0,0 +1,474 @@
use anyhow::anyhow;
use candle_core::{DType, Device, Shape, Tensor, WithDType};
use candle_transformers::models::stable_diffusion::schedulers::Scheduler;
use candle_transformers::models::stable_diffusion::utils::linspace;
use log::error;
/*
Notes on LCM implementation:
- Robust technical comparison of samplers: https://stable-diffusion-art.com/samplers/
- Apple Swift LCM Implementation ticket: https://github.com/apple/ml-stable-diffusion/issues/319
- Reference Swift Implementation: https://github.com/GuernikaCore/Schedulers/blob/main/Sources/Schedulers/LCMScheduler.swift
Candle is missing several "batteries" we'll have to implement, and some we might want to upstream:
- t[::-1] (syntax to reverse the tensor)
- torch/numpy.cumprod :
- https://numpy.org/doc/stable/reference/generated/numpy.cumprod.html
- https://pytorch.org/docs/stable/generated/torch.cumprod.html
Remaining work:
- [ ] implement add_noise
- [ ] implement cumprod
- [ ] initialize step_index / begin_index
- [ ] find out how `set_timestamps` is called in diffusers and see if we want to change it to CTOR param in candle
- [ ] try to plug this into candle and run
- [ ] fix timesteps() method to cache values
- [ ] compare with diffusers
- [ ] compare with candle schedulers
- [ ] compare with swift schedulers
- [ ] test + unit test
*/
struct LcmScheduler {
alphas: Tensor,
alphas_cumprod: Tensor,
final_alpha_cumprod: Tensor,
betas: Tensor,
timesteps: Tensor,
// Standard deviation of the initial noise distribution
// NB: Only in diffusers, and seems to only hold the value "1.0". Swift implementation omits it.
init_noise_sigma: f64,
// TODO: This is set by the set_timesteps() method after construction in python.
// We might want to handle this upfront in the constructor ourselves
num_inference_steps: Option<usize>,
_step_index: Option<usize>,
_begin_index: Option<usize>,
}
impl LcmScheduler {
pub fn new(
num_train_timestamps: i64,
beta_start: f64,
beta_end: f64,
beta_schedule: &str,
trained_betas: Option<Tensor>,
set_alpha_to_one: bool,
rescale_betas_zero_snr: bool,
device: &Device,
) -> anyhow::Result<Self> {
let betas;
if let Some(trained) = trained_betas {
betas = trained;
} else {
betas = match beta_schedule {
"scaled_linear" => {
// this schedule is very specific to the latent diffusion model.
let t = linspace(
beta_start.powf(0.5),
beta_end.powf(0.5),
num_train_timestamps as usize
)?;
t.to_device(device)?
}
_ => {
return Err(anyhow!("schedule {beta_schedule} is not implemented"))
}
};
}
if rescale_betas_zero_snr {
return Err(anyhow!("rescale_betas_zero_snr is not used upstream"));
}
let alphas = (1.0 - &betas)?;
// TODO: Implement cumprod: https://github.com/huggingface/candle/issues/1646
// https://numpy.org/doc/stable/reference/generated/numpy.cumprod.html
error!("THIS IS USING CUMSUM, NOT CUMPROD. WE NEED TO IMPLEMENT CUMPROD.");
// self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)
let alphas_cumprod = alphas.cumsum(0)?;
// TODO: Check implementation
// At every step in ddim, we are looking into the previous alphas_cumprod
// For the final step, there is no previous alphas_cumprod because we are already at 0
// `set_alpha_to_one` decides whether we set this parameter simply to one or
// whether we use the final alpha of the "non-previous" one.
let final_alpha_cumprod = if set_alpha_to_one {
initialize_scalar_tensor(1.0, device)? // NB: `set_alpha_to_one` is false in python
} else {
alphas_cumprod.get(0)?
};
// standard deviation of the initial noise distribution
let init_noise_sigma = 1.0;
let timesteps = initialize_timesteps(num_train_timestamps, device)?;
Ok(Self {
alphas,
alphas_cumprod,
final_alpha_cumprod,
betas,
init_noise_sigma,
timesteps,
num_inference_steps: None,
_step_index: None,
_begin_index: None,
})
}
pub fn set_begin_index(&mut self, begin_index: usize) {
self._begin_index = Some(begin_index);
}
pub fn init_step_index(&mut self, timestep: usize) -> candle_core::Result<()> {
match self._begin_index {
None => {
todo!("implement begin index none branch")
}
Some(begin_index) => {
self._step_index = Some(begin_index);
}
}
Ok(())
}
pub fn set_timesteps(&mut self, num_inference_steps: usize, device: &Device) -> candle_core::Result<()> {
// NB: The diffusers version of this method passes `None` for `original_inference_steps` and `timesteps`.
// It passes strength = 1.0, and config.original_inference_steps = 50.
const STRENGTH : f64 = 1.0;
const CONFIG_ORIGINAL_INFERENCE_STEPS : usize = 50;
const CONFIG_NUM_TRAIN_TIMESTEPS : usize = 1000;
let original_steps = CONFIG_ORIGINAL_INFERENCE_STEPS;
// LCM Timesteps Setting
// The skipping step parameter k from the paper.
let k = (CONFIG_NUM_TRAIN_TIMESTEPS as f64 / original_steps as f64).floor();
let end = (original_steps as f64 * STRENGTH) as i64 + 1;
let lcm_origin_timesteps = ((Tensor::arange::<i64>(1, end, device)? * k)? - 1.0)?;
if num_inference_steps > CONFIG_NUM_TRAIN_TIMESTEPS {
return Err(candle_core::Error::Msg("num_inference_steps cannot be larger than train timesteps".to_string()));
}
let skipping_step = (lcm_origin_timesteps.dims().len() as f64 / num_inference_steps as f64).floor() as usize;
self.num_inference_steps = Some(num_inference_steps);
// TODO: This is not efficient
let lcm_origin_timesteps = lcm_origin_timesteps.to_vec1::<i64>()?
.into_iter()
.map(i64::from)
.rev()
.collect::<Vec<i64>>();
// TODO: This might not be exact. We should unit test this
let inference_indices = linspace(0.0, lcm_origin_timesteps.len() as f64, num_inference_steps)?;
let inference_indices = inference_indices.floor()?
.to_dtype(DType::I64)?;
let timestamps = {
// TODO: This code is awful
let inference_indices_t: Vec<usize> = inference_indices.to_vec1::<i64>()?
.into_iter()
.map(|n| n as usize)
.collect();
let mut timesteps: Vec<i64> = Vec::with_capacity(inference_indices.dims().len());
for i in inference_indices_t {
if let Some(val) = lcm_origin_timesteps.get(i) {
timesteps.push(*val);
}
}
let shape = Shape::from_dims(&[timesteps.len()]);
Tensor::from_vec(timesteps, shape, device)?
.to_dtype(DType::I64)?
.to_device(device)?
};
self.timesteps = timestamps;
Ok(())
}
}
impl Scheduler for LcmScheduler {
fn timesteps(&self) -> &[usize] {
// TODO: We can't use the implementation from DDIM. This method requires
// (1) an infallible response and (2) a reference we can't dynamically allocate. We must
// cache this somewhere.
&[]
}
fn add_noise(&self, original_samples: &Tensor, noise: Tensor, timestep: usize) -> candle_core::Result<Tensor> {
// TODO: Make sure alphas_cumprod and timestep have same device and dtype as original_samples
// Move the self.alphas_cumprod to device to avoid redundant CPU to GPU data movement
// for the subsequent add_noise calls
let mut alphas_cumprod = self.alphas_cumprod.to_device(original_samples.device())?;
alphas_cumprod = alphas_cumprod.to_dtype(original_samples.dtype())?;
//let timesteps = timesteps.to_device(original_samples.device())?;
let sqrt_alpha_prod = alphas_cumprod.get(timestep)?.powf(0.5)?;
let mut sqrt_alpha_prod = sqrt_alpha_prod.flatten_all()?; // TODO: Verify that flatten_all works here
while sqrt_alpha_prod.dims().len() < original_samples.dims().len() {
// TODO: Verify that this works for `unsqueeze(-1)`
sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(sqrt_alpha_prod.dims().len())?;
}
let sqrt_one_minus_alpha_prod = (1.0 - alphas_cumprod.get(timestep)?)?.powf(0.5)?;
let mut sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten_all()?; // TODO: Verify flatten all works here.
while sqrt_one_minus_alpha_prod.dims().len() < original_samples.dims().len() {
// TODO: Verify that this works for `unsqueeze(-1)`
sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(sqrt_one_minus_alpha_prod.dims().len())?;
}
let noisy_samples = ((sqrt_alpha_prod * original_samples)? + (sqrt_one_minus_alpha_prod * noise)?)?;
Ok(noisy_samples)
}
fn init_noise_sigma(&self) -> f64 {
self.init_noise_sigma
}
/// Ensures interchangeability with schedulers that need to scale the denoising model input
/// depending on the current timestep. (For LCM, this is a pass through no-op!)
fn scale_model_input(&self, sample: Tensor, _timestep: usize) -> candle_core::Result<Tensor> {
Ok(sample) // NB: The diffusers scheduling_lcm.py implementation is a pass through no-op !
}
/// Predict the sample from the previous timestep by reversing the SDE. This function
/// propagates the diffusion process from the learned model outputs (most often the
/// predicted noise).
fn step(&mut self, model_output: &Tensor, timestep: usize, sample: &Tensor) -> candle_core::Result<Tensor> {
if self.num_inference_steps.is_none() {
return Err(candle_core::Error::Msg(
"Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler"
.to_string()));
}
if self._step_index.is_none() {
self.init_step_index(timestep)?;
}
let step_index = self._step_index.clone().expect("unsafe unwrap; should exist per above");
// 1. Get previous step value
let prev_step_index = step_index + 1;
let prev_timestep;
if prev_step_index < self.timesteps.elem_count() {
prev_timestep = self.timesteps.get(timestep)?.to_scalar::<i64>()? as usize;
} else {
prev_timestep = timestep;
}
// 2. Compute alphas, betas
let alpha_prod_t = self.alphas_cumprod.get(timestep)?;
let alpha_prod_t_prev = if prev_timestep >= 0 {
self.alphas_cumprod.get(prev_timestep)?
} else {
self.final_alpha_cumprod.clone()
};
let beta_prod_t = (1. - &alpha_prod_t)?;
let beta_prod_t_prev = (1. - &alpha_prod_t_prev)?;
// 3. Get scaling for boundary conditions
// NB: timestep_scaling seems to be always set to 10.0 in diffusers examples, but it is passed
// via a config object. Hardcoding this for now until we need to change it.
const TIMESTEP_SCALING : f64 = 10.0;
let (c_skip, c_out) = get_scalings_for_boundary_condition_discrete(timestep, TIMESTEP_SCALING);
// 4. Compute the predicted original sample x_0 based on the model parameterization
// NB: Prediction type is "epsilon" in the diffusers examples, but that is configurable.
// This is the only one we'll implement for now.
let predicted_original_sample = ((sample - (beta_prod_t.sqrt()? * model_output)?)? / &alpha_prod_t.sqrt()?)?;
// 5. (skip clip/threshold - not used in diffusers example)
// 6. Denoise model output using boundary conditions
let denoised = ((c_out * predicted_original_sample)? + (c_skip * sample)?)?;
// TODO: Step 7. We have to return the previous example somehow.
// Diffusers tolerates it not being sent back, but the quality degrades as if one step is absent.
Ok(denoised)
}
}
fn initialize_scalar_tensor<D: WithDType>(value: D, device: &Device) -> anyhow::Result<Tensor> {
let scalar = Tensor::from_vec(vec![value], Shape::from(vec![]), device)?;
Ok(scalar)
}
// TODO(bt,2025-02-23): Use native implementations
fn initialize_timesteps(num_train_timesteps: i64, device: &Device) -> anyhow::Result<Tensor> {
// Testing LCM scheduler assumptions:
// [::-1] is shorthand for reversing a numpy vector, eg. [0, 1,2,3,4] -> [4, 3, 2, 1, 0]
// self.timesteps = torch.from_numpy(np.arange(0, num_train_timesteps)[::-1].copy().astype(np.int64))
//let tensor = Tensor::arange::<i64>(0, 5, &Device::Cpu)?; // This works, but we can't reverse/flip it!
// https://github.com/tracel-ai/burn/pull/1468 flip impl? Will this work in place?
// https://github.com/huggingface/candle/issues/1875
let data = (0..num_train_timesteps)
.map(i64::from)
.rev()
.collect();
let shape = Shape::from_dims(&[num_train_timesteps as usize]);
let tensor = Tensor::from_vec(data, shape, device)?;
Ok(tensor)
}
fn get_scalings_for_boundary_condition_discrete(timestep: usize, timestep_scaling: f64) -> (f64, f64) {
let sigma_data = 0.5f64; // Default 0.5
let scaled_timestep = timestep as f64 * timestep_scaling;
let c_skip = sigma_data.powf(2.0) / (scaled_timestep.powf(2.0) + sigma_data.powf(2.0));
let c_out = scaled_timestep / (scaled_timestep.powf(2.0) + sigma_data.powf(2.0)).powf(0.5);
(c_skip, c_out)
}
// The Candle sources, and moreover, the Candle tests, are a great source of information for how the library works.
//assert_eq!(t.i(..-1)?, 1.0);
#[cfg(test)]
mod tests {
use crate::ml::lcm_scheduler::{initialize_scalar_tensor, initialize_timesteps};
use crate::ml::lcm_scheduler::tests::test_helper_fn::*;
use candle_core::Device;
mod helper_function_unit_tests {
use crate::ml::lcm_scheduler::get_scalings_for_boundary_condition_discrete;
use super::*;
#[test]
fn test_initialize_scalar_tensor() -> anyhow::Result<()> {
let t = initialize_scalar_tensor(1.0, &Device::Cpu)?;
let empty: [usize; 0] = [];
assert_eq!(t.dims(), &empty);
assert_eq!(t.to_vec0::<f64>()?, 1.0);
Ok(())
}
#[test]
fn test_initialize_timesteps() -> anyhow::Result<()> {
let tensor = initialize_timesteps(5, &Device::Cpu)?;
assert_eq!(tensor.to_vec1::<i64>()?, &[4, 3, 2, 1,0]);
Ok(())
}
#[test]
fn test_get_scalings_for_boundary_condition_discrete() -> anyhow::Result<()> {
// Base cases
let v = get_scalings_for_boundary_condition_discrete(0, 0.0);
assert_eq!(v, (1.0, 0.0));
let v = get_scalings_for_boundary_condition_discrete(1, 0.0);
assert_eq!(v, (1.0, 0.0));
let v = get_scalings_for_boundary_condition_discrete(0, 1.0);
assert_eq!(v, (1.0, 0.0));
// Production code has "timestep_scaling" = 10.0
let v = get_scalings_for_boundary_condition_discrete(0, 10.0);
assert_eq!(v, (1.0, 0.0));
// Production code has "timestep_scaling" = 10.0
let v = get_scalings_for_boundary_condition_discrete(1, 10.0);
assert!((v.0 - 0.0024937655860349127).abs() < 0.01);
assert!((v.1 - 0.9987523388778445).abs() < 0.01);
// TODO: More test cases -
//let v = get_scalings_for_boundary_condition_discrete(10, 10.0);
//let v = get_scalings_for_boundary_condition_discrete(100, 10.0);
Ok(())
}
}
mod confirm_various_pytorch_behaviors {
use candle_core::IndexOp;
use super::*;
// NB: Non-unit tests, just to test understanding while porting code
#[test]
fn unsqueeze_zero() -> anyhow::Result<()> {
let t = tensor_1234()?;
let x = t.unsqueeze(0)?;
assert_eq!(x.to_vec2::<i64>()?, &[&[1, 2, 3, 4]]);
Ok(())
}
#[test]
fn unsqueeze_simulating_negative_one() -> anyhow::Result<()> {
let t = tensor_1234()?;
// NB: It looks like this matches the behavior of unsqueeze(-1)
// Per: https://pytorch.org/docs/stable/generated/torch.unsqueeze.html
let x = t.unsqueeze(t.dims().len())?;
assert_eq!(x.to_vec2::<i64>()?, &[&[1],&[2],&[3],&[4]]);
Ok(())
}
#[test]
fn one_minus_vector() -> anyhow::Result<()> {
let t = tensor_01234()?;
let x = (1. - t)?;
assert_eq!(x.rank(), 1); // NB: 1-dimensional output
assert_eq!(x.to_vec1::<i64>()?, &[1, 0, -1, -2, -3]);
Ok(())
}
// #[test]
// fn reverse_vector() -> anyhow::Result<()> {
// let t = tensor_1234()?;
// let x = t.i(5..0)?;
// assert_eq!(x.rank(), 1); // NB: 1-dimensional output
// assert_eq!(x.to_vec1::<i64>()?, &[1, 0, -1, -2, -3]);
// Ok(())
// }
}
pub mod test_helper_fn {
use candle_core::{Device, Shape, Tensor};
pub fn tensor_1234() -> anyhow::Result<Tensor> {
// Return an example tensor
let data: Vec<i64> = vec![1, 2, 3, 4];
let t = Tensor::from_vec(data, Shape::from(vec![4]), &Device::Cpu)?;
assert_eq!(t.to_vec1::<i64>()?, &[1, 2, 3, 4]);
Ok(t)
}
pub fn tensor_01234() -> anyhow::Result<Tensor> {
// Return an example tensor
let data: Vec<i64> = vec![0, 1, 2, 3, 4];
let t = Tensor::from_vec(data, Shape::from(vec![5]), &Device::Cpu)?;
assert_eq!(t.to_vec1::<i64>()?, &[0, 1, 2, 3, 4]);
Ok(t)
}
}
}
+2
View File
@@ -1,4 +1,6 @@
pub mod cumprod;
pub mod image;
pub mod lcm_scheduler;
pub mod model_cache;
pub mod model_file;
pub mod models;
+10
View File
@@ -4525,6 +4525,8 @@
},
"node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -5265,6 +5267,8 @@
},
"node_modules/npm/node_modules/cross-spawn/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5313,6 +5317,8 @@
},
"node_modules/npm/node_modules/debug/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true,
"inBundle": true,
"license": "MIT"
@@ -7139,6 +7145,8 @@
},
"node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -7176,6 +7184,8 @@
},
"node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"inBundle": true,
"license": "MIT",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+64 -16
View File
@@ -27,8 +27,9 @@ import { NavigateFunction } from "react-router-dom";
import { LoadingVideosProvider } from "./EngineUtitlities/LoadingVideosProvider";
import { VideoExtractionHandler } from "./EngineUtitlities/VideoExtractionHandler/VideoExtractionHandler";
import { RealTimeDrawEngine, ShapeType } from "./RenderingPrimitives/RealTimeDrawEngine";
import { RealTimeDrawEngine } from "./RenderingPrimitives/RealTimeDrawEngine";
import { NodeColor } from "~/signals/uiEvents/toolbarNode";
import { ShapeType } from "./Nodes";
// for testing loading files from system
// import { FileUtilities } from "./FileUtilities/FileUtilities";
@@ -180,8 +181,8 @@ export class Engine {
this.setupEventSystem();
this.setAppMode(AppModes.SELECT);
}
@@ -364,7 +365,7 @@ export class Engine {
// Listen to Toolbar Main
uiEvents.toolbarMain.UNDO.onClick(() => this.undoStackManager.undo());
uiEvents.toolbarMain.REDO.onClick(() => this.undoStackManager.redo());
uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
await this.realTimeDrawEngine.saveOutput();
@@ -398,7 +399,7 @@ export class Engine {
this.addImage(image);
});
uiEvents.onAddTextToEngine((textdata) => {
this.addText(textdata);
@@ -430,7 +431,7 @@ export class Engine {
}
}
});
uiEvents.promptEvents.onPromptStrengthChanged((strength) => {
this.realTimeDrawEngine.currentStrength = strength;
});
@@ -439,21 +440,32 @@ export class Engine {
this.realTimeDrawEngine.currentPrompt = prompt;
});
uiEvents.onAddShapeToEngine((shapeData)=> {
uiEvents.onAddShapeToEngine((shapeData) => {
switch (shapeData.shape) {
case "circle":
this.addShape(ShapeType.Circle, 100);
this.addShape(ShapeType.CIRCLE, 100);
break;
case "square":
this.addShape(ShapeType.Square, 100);
case "square":
this.addShape(ShapeType.SQUARE, 100);
break;
case "triangle":
this.addShape(ShapeType.Triangle, 100);
this.addShape(ShapeType.TRIANGLE, 100);
break;
}
});
uiEvents.toolbarNode.color.onConfirmChanged((nodeColor) => {
console.log("Color change event triggered with:", nodeColor);
if (!nodeColor) {
console.warn("nodeColor is undefined or null");
return;
}
if (!nodeColor.kNodeId) {
console.warn("nodeColor.kNodeId is undefined or null");
return;
}
this.changeNodeColor(nodeColor);
})
}
disableAllButtons() {
@@ -489,7 +501,7 @@ export class Engine {
}
// Sandbox is quickly a way to test your idea.
public async sandbox() {}
public async sandbox() { }
public onMessage(event: MessageEvent) {
console.log("Message From Shared Worker");
@@ -536,6 +548,40 @@ export class Engine {
this.addKeyboardShortcuts();
}
private changeNodeColor(nodeColor: NodeColor) {
console.log('Changing node color:', nodeColor);
// Find the node that is selected with the id
const idSelector = '#' + nodeColor.kNodeId;
const selectedNode = this.mediaLayer.findOne(idSelector);
console.log('Selected node:', selectedNode);
console.log('ID selector:', idSelector);
console.log('Media layer:', this.mediaLayer);
// TODO: Change the color of the node
// Turns out shapes are image nodes... how do we change the image's fill??
if (selectedNode) {
// Get the shape node from the real time drawing engine's image nodes
const shapeNode = this.realTimeDrawEngine.findImageNodeById(nodeColor.kNodeId);
if (shapeNode && shapeNode instanceof ShapeNode) {
// Create new shape node with updated color
const newShapeNode = new ShapeNode({
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
shapeType: shapeNode.shapeType,
size: shapeNode.size,
color: nodeColor.color,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager
});
newShapeNode.kNode.position(shapeNode.kNode.position());
newShapeNode.kNode.zIndex(shapeNode.kNode.zIndex());
// Remove old node and add new one
shapeNode.kNode.destroy();
this.commandManager.createNode(newShapeNode);
}
}
}
public addText(textNodeData: TextNodeData) {
const textNode = new TextNode({
textNodeData: textNodeData,
@@ -558,8 +604,10 @@ export class Engine {
selectionManagerRef: this.selectionManager
});
shapeNode.kNode.zIndex(1); // Replace desiredZIndex with a number
this.commandManager.createNode(shapeNode);
console.debug("Added shapenode:", shapeNode);
console.debug("Added node's ID:", shapeNode.kNode._id);
//this.realTimeDrawEngine.addNodes(shapeNode);
}
@@ -617,7 +665,7 @@ export class Engine {
if (
longestVideoNode === undefined ||
node.videoComponent.duration >
longestVideoNode.videoComponent.duration
longestVideoNode.videoComponent.duration
) {
longestVideoNode = node;
}
@@ -58,6 +58,10 @@ export class CommandManager {
renderEngineRef: this.renderEngineRef,
});
this.undoStackManagerRef.executeCommand(command);
// Set the kNode's manual id to same as kNode's internal _id
// This helps identify the kNode in the Konva layer with find
node.kNode.id(node.kNode._id.toString());
}
deleteNodes() {
const nodes = this.selectionManagerRef.getSelectedNodes();
@@ -1,7 +1,6 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { Position, Size, NodeData, TransformationData } from "../types";
import { BaseNode } from "./BaseNode";
import { NodeType } from "./constants";
import { NodeUtilities } from "./NodeUtilities";
import { NetworkedNode } from "./NetworkedNode";
@@ -24,6 +23,13 @@ interface ShapeNodeConstructor {
}
export class ShapeNode extends NetworkedNode {
retry(): void {
throw new Error("Method not implemented.");
}
public shapeType: ShapeType;
public size: number;
constructor({
canvasPosition,
@@ -159,6 +165,9 @@ export class ShapeNode extends NetworkedNode {
kNode: shape,
});
this.shapeType = shapeType;
this.size = size;
// Add shape to group
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: canvasPosition,
@@ -3,4 +3,4 @@ export { BaseNode } from "./BaseNode";
export { ImageNode } from "./ImageNode";
export { VideoNode } from "./VideoNode";
export { TextNode } from "./TextNode";
export { ShapeNode } from "./ShapeNode";
export { ShapeNode , ShapeType} from "./ShapeNode";
@@ -9,6 +9,7 @@ import {
calculateContextualsPosition,
getImageNodeButtonStates,
getMultiSelectButtonStates,
getShapeNodeButtonStates,
getTextNodeButtonStates,
getVideoNodeButtonStates,
} from "./ToolbarNodeUtilities";
@@ -271,22 +272,31 @@ export class SelectionManager {
return; // no node
}
if (this.selectedNodes.size === 1) {
if (node instanceof ImageNode || node instanceof ShapeNode) {
if (node instanceof ImageNode) {
showOrUpdate({
knodeIds: [node.kNode.id()],
locked: node.isLocked(),
buttonStates: getImageNodeButtonStates({ locked: node.isLocked() }),
});
}
else if (node instanceof ShapeNode) {
console.debug("Selected node is of type ShapeNode", node.kNode.id())
showOrUpdate({
knodeIds: [node.kNode.id()],
locked: node.isLocked(),
buttonStates: getShapeNodeButtonStates({ locked: node.isLocked() }),
color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000",
});
}
if (node instanceof TextNode) {
else if (node instanceof TextNode) {
showOrUpdate({
// TODO: Add ID to identify/mutate the color?
knodeIds: [node.kNode.id()],
locked: node.isLocked(),
buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }),
color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000",
});
}
if (node instanceof VideoNode) {
else if (node instanceof VideoNode) {
showOrUpdate({
locked: node.isLocked(),
buttonStates: getVideoNodeButtonStates({ locked: node.isLocked() }),
@@ -317,7 +327,7 @@ export class SelectionManager {
this.hideContextComponents();
return; // no nodes
}
// console.log("SelectionManager > updateContextComponents for node:", node);
const coord = calculateContextualsPosition(
this.nodeTransformerRef.getKonvaNode(),
@@ -152,6 +152,50 @@ export function getVideoNodeButtonStates(
{} as ContextualToolbarProps["buttonStates"],
);
}
export function getShapeNodeButtonStates(
props: { locked?: boolean | "unknown" } = {},
) {
const ButtonNames = ToolbarNodeButtonNames;
return Object.values(ButtonNames).reduce(
(buttonStates, buttonName) => {
// transform button depends on locked state
if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) {
buttonStates.TRANSFORM = {
disabled: props.locked === "unknown" || props.locked === true,
hidden: false,
active: true,
};
return buttonStates;
}
// hidden buttons
if (
buttonName === ButtonNames.AI_STYLIZE ||
buttonName === ButtonNames.SEGMENTATION ||
buttonName === ButtonNames.DOWNLOAD ||
buttonName === ButtonNames.CHROMA
) {
buttonStates[buttonName] = {
disabled: true,
hidden: true,
active: false,
};
return buttonStates;
}
// all other buttons
buttonStates[buttonName] = {
disabled: false,
hidden: false,
active: false,
};
return buttonStates;
},
{} as ContextualToolbarProps["buttonStates"],
);
}
export function getMultiSelectButtonStates(
props: { locked?: boolean | "unknown" } = {},
) {
@@ -6,18 +6,14 @@ import { Group } from "konva/lib/Group";
import { invoke } from "@tauri-apps/api/core";
import { FileUtilities } from "../FileUtilities/FileUtilities";
import { ImageNode, VideoNode, TextNode, ShapeNode } from "../Nodes";
import { ImageNode, VideoNode, TextNode, ShapeNode, ShapeType } from "../Nodes";
import { MediaNode } from "../types";
import { RenderTask } from "./RenderTask";
import { OffScreenSceneCanvas } from "./OffScreenSceneCanvas";
// https://www.aiseesoft.com/resource/phone-aspect-ratio-screen-resolution.html#:~:text=16%3A9%20Aspect%20Ratio
export enum ShapeType {
Square = "square",
Triangle = "triangle",
Circle = "circle"
}
export class RealTimeDrawEngine {
private videoNodes: VideoNode[];
private imageNodes: (ImageNode | TextNode | ShapeNode)[];
@@ -80,7 +76,6 @@ export class RealTimeDrawEngine {
this.offScreenCanvas.width = this.width;
this.offScreenCanvas.height = this.height;
// this is the whole canvas
this.mediaLayerRef = mediaLayerRef;
@@ -123,8 +118,79 @@ export class RealTimeDrawEngine {
// send back
this.captureCanvas.setZIndex(0);
this.previewCanvas.setZIndex(1);
// Add mouse events for preview canvas copying
//this.previewCopyListener();
}
public previewCopyListener() {
this.previewCanvas.on('mousedown touchstart', () => {
if (!this.outputBitmap) {
console.log("No preview image to copy");
return;
}
// TODO create it as a node.
// const imageNode = new ImageNode({
// mediaLayerRef: this.mediaLayer,
// canvasPosition: this.renderEngine.captureCanvas.position(),
// canvasSize: this.renderEngine.captureCanvas.size(),
// imageFile: imageFile,
// selectionManagerRef: this.selectionManager,
// });
// Create draggable preview copy
const previewCopy = new Konva.Image({
x: this.previewCanvas.x(),
y: this.previewCanvas.y(),
width: this.width,
height: this.height,
image: this.outputBitmap,
draggable: true,
listening: true
});
this.mediaLayerRef.add(previewCopy);
previewCopy.moveToTop();
this.mediaLayerRef.draw();
// Start dragging immediately
previewCopy.startDrag();
// Handle drag events
previewCopy.on('dragmove', () => {
this.mediaLayerRef.draw();
});
previewCopy.on('dragend', () => {
const previewBox = previewCopy.getClientRect();
const captureBox = this.captureCanvas.getClientRect();
if (Konva.Util.haveIntersection(previewBox, captureBox)) {
// Snap to capture canvas position
previewCopy.position({
x: this.captureCanvas.x(),
y: this.captureCanvas.y()
});
this.mediaLayerRef.batchDraw();
} else {
// Remove if dropped outside capture area
previewCopy.destroy();
this.mediaLayerRef.batchDraw();
}
});
});
}
public findImageNodeById(id: string): (ImageNode | TextNode | ShapeNode | undefined) {
return this.imageNodes.find(node => {
if (node.kNode) {
return node.kNode.id() === id;
}
return false;
});
}
async updateCaptureCanvas(
width: number | undefined,
height: number | undefined,
@@ -467,7 +533,7 @@ export class RealTimeDrawEngine {
})) as ImageBitmap;
// Test code
if (true) {
if (false) {
this.outputBitmap = bitmap;
this.previewCanvas.image(bitmap);
this.isProcessing = false;
@@ -26,6 +26,10 @@ export type VideoNodeData = {
extractionUrl?: string;
};
export type ShapeNodeData = {
shape: "triangle" | "square" | "circle";
}
export type NodeData = {
type: NodeType;
transform: TransformationData;
@@ -50,6 +50,20 @@ export const ContextualToolbarNode = () => {
};
}, []);
const handleColorChange = (color: string) => {
// Don't do anything if no node is known to be bound
if (uiAccess.toolbarNode.signal.value.knodeIds.length < 1) {
return;
}
// Update the UI component
uiAccess.toolbarNode.setColor(color);
// Signal the engine with the node ID (just grab the first -and expected only- node)
const nodeID = uiAccess.toolbarNode.signal.value.knodeIds[0];
dispatchUiEvents.toolbarNode.setSelectedColor({ kNodeId: nodeID, color });
}
const boundedTopPosition = Math.min(position.y, winSize.height - 164);
return (
<Transition
@@ -72,7 +86,7 @@ export const ContextualToolbarNode = () => {
dispatchUiEvents.toolbarNode.lock(e);
}}
color={uiAccess.toolbarNode.signal.value.color}
onColorChange={(color) => { uiAccess.toolbarNode.setColor(color) }}
onColorChange={handleColorChange}
/>
</Transition>
);
@@ -78,7 +78,7 @@ const Landing = () => {
</div>
</div>
</div>
<div className="col-span-12 mt-12 aspect-video w-full transform overflow-hidden rounded-xl border border-white/[15%] bg-transparent md:mt-16 xl:col-span-9 xl:col-start-4 xl:mt-0">
<div className="col-span-12 mt-12 aspect-video w-full transform overflow-hidden rounded-xl border border-white/[16%] bg-transparent md:mt-16 xl:col-span-9 xl:col-start-4 xl:mt-0">
<img
src="/images/landing_hero.png"
alt="AI Video Creation"
@@ -118,6 +118,52 @@ const Landing = () => {
</div>
))}
</div>
{/* Installation Steps Section */}
<div className="mt-36">
<h1 className="mb-12 text-5xl font-bold">
How to install Mira AI Editor
</h1>
<div className="grid gap-6 md:grid-cols-3">
<div className="rounded-xl bg-[#1C1C20] p-8">
<div className="mb-4 w-fit rounded-full bg-white/15 px-3 py-1 text-sm font-medium text-white">
STEP 1
</div>
<h3 className="mb-4 text-2xl font-semibold">Download the app</h3>
<p className="mb-6 text-gray-400">
Download and install our application to start creating amazing
AI-powered content.
</p>
<Button className="rounded-lg bg-[#2D81FF] px-4 py-2 text-sm font-medium hover:bg-[#438AF6]">
Download App
</Button>
</div>
<div className="rounded-xl bg-[#1C1C20] p-8">
<div className="mb-4 w-fit rounded-full bg-white/15 px-3 py-1 text-sm font-medium text-white">
STEP 2
</div>
<h3 className="mb-4 text-2xl font-semibold">Create an account</h3>
<p className="mb-6 text-gray-400">
Sign up for an account to access all features and start your
journey.
</p>
<Button className="text-sm font-medium" variant="secondary">
Sign up
</Button>
</div>
<div className="rounded-xl bg-[#1C1C20] p-8">
<div className="mb-4 w-fit rounded-full bg-white/15 px-3 py-1 text-sm font-medium text-white">
STEP 3
</div>
<h3 className="mb-4 text-2xl font-semibold">Start creating</h3>
<p className="mb-6 text-gray-400">
Begin creating with our realtime AI editor!
</p>
</div>
</div>
</div>
</div>
<Footer />
@@ -97,7 +97,7 @@ export const ToolbarNode = ({
);
} else {
return (
<ColorPicker color={color} onChange={onColorChange} faIcon={faPalette} borderStyle="" />
<ColorPicker color={color} onChange={onColorChange} faIcon={faPalette} borderStyle="" key={idx} />
)
}
})}
@@ -10,7 +10,7 @@ type ButtonStates = {
};
};
export interface ContextualToolbarProps extends ContextualUi {
knodeIds: number[];
knodeIds: string[];
disabled: boolean;
downloadUrl?: string;
locked: boolean | "unknown";
@@ -1,5 +1,5 @@
import { signal, effect } from "@preact/signals-react";
import { TextNodeData } from "~/KonvaApp/types";
import { ShapeNodeData, TextNodeData } from "~/KonvaApp/types";
// ADDING IMAGES
const stagedImage = signal<File | null>(null);
@@ -50,8 +50,6 @@ const onAddTextToEngine = (callback: (data: TextNodeData) => void) => {
};
// Add shapes
// TODO: Remove this line and replace with actual shape data that engine needs
type ShapeNodeData = any;
const stagedShape = signal<ShapeNodeData | null>(null);
const addShapeToEngine = (data: ShapeNodeData) => {
stagedShape.value = data;
@@ -44,6 +44,11 @@ const buttonEventsRecords = Object.values(ButtonNames).reduce(
const buttonEventsHandlers = Object.values(ButtonNames).reduce(
(acc, buttonName) => {
// We define custom events/handlers for color changes
if (buttonName === ButtonNames.COLOR) {
return acc;
}
acc[buttonName] = {
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => {
const { eventSignal, effectCleanup, lastEventTimestamp } =
@@ -111,9 +116,27 @@ const retryEventHandler = (callback: MouseEventHandler<HTMLButtonElement>) => {
const retryDispatcher = (e: React.MouseEvent<HTMLButtonElement>) => {
retryEvent.value = e;
};
export type NodeColor = {
kNodeId: string;
color: string;
}
const selectedColorEvent = signal<NodeColor | null>();
const setSelectedColor = (nodeColor: NodeColor) => {
selectedColorEvent.value = nodeColor;
}
const onSelectedColorChanged = (callback: (color: NodeColor) => void) => {
effect(() => {
if (selectedColorEvent.value) {
callback(selectedColorEvent.value);
}
});
};
export const dispatchers = {
lock: lockDispatcher,
retry: retryDispatcher,
setSelectedColor,
...buttonDispatchers,
};
export const eventsHandlers = {
@@ -123,5 +146,8 @@ export const eventsHandlers = {
retry: {
onClick: retryEventHandler,
},
color: {
onConfirmChanged: onSelectedColorChanged,
},
...buttonEventsHandlers,
};