diff --git a/.gitignore b/.gitignore
index 078a9b6..1317219 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,python,react
# Edit at https://www.toptal.com/developers/gitignore?templates=node,python,react
+workflow_templates/
js/output.css
+*.task
### Node ###
# Logs
diff --git a/__init__.py b/__init__.py
index 1fcb5a2..b34764f 100644
--- a/__init__.py
+++ b/__init__.py
@@ -43,7 +43,29 @@ def append_to_sys_path(path):
if path not in sys.path:
sys.path.append(path)
-folder_paths.folder_names_and_paths["sams"] = ([os.path.join(folder_paths.models_dir, "sams")], folder_paths.supported_pt_extensions)
+
+folder_paths.folder_names_and_paths["sams"] = (
+ [os.path.join(folder_paths.models_dir, "sams")],
+ folder_paths.supported_pt_extensions,
+)
+
+
+def download_model(url, save_path):
+ response = requests.get(url, stream=True)
+ response.raise_for_status()
+ file_size = int(response.headers.get("Content-Length", 0))
+ chunk_size = 1024
+ num_bars = int(file_size / chunk_size)
+
+ with open(save_path, "wb") as f:
+ for chunk in tqdm(
+ response.iter_content(chunk_size=chunk_size),
+ total=num_bars,
+ unit="KB",
+ desc=url.split("/")[-1],
+ ):
+ f.write(chunk)
+
def download_sam_model():
model_dir = get_folder_paths("sams")[0]
@@ -56,24 +78,32 @@ def download_sam_model():
if "sam_vit_h_4b8939.pth" not in files:
print("Downloading sam model...")
url = "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth"
- response = requests.get(url, stream=True)
- response.raise_for_status()
- file_size = int(response.headers.get("Content-Length", 0))
- chunk_size = 1024
- num_bars = int(file_size / chunk_size)
-
- with open(f"{model_dir}/sam_vit_h_4b8939.pth", "wb") as f:
- for chunk in tqdm(
- response.iter_content(chunk_size=chunk_size),
- total=num_bars,
- unit="KB",
- desc=url.split("/")[-1],
- ):
- f.write(chunk)
+ download_model(url, f"{model_dir}/sam_vit_h_4b8939.pth")
download_sam_model()
+
+def download_face_and_pose_landmarker():
+ model_dir = os.path.join(ag_path, "mediapipe_models")
+ if not os.path.isdir(model_dir):
+ os.makedirs(model_dir)
+
+ model_path = os.path.join(model_dir, "face_landmarker.task")
+ if not os.path.isfile(model_path):
+ print("Downloading face landmarker model...")
+ url = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task"
+ download_model(url, model_path)
+
+ model_path = os.path.join(model_dir, "pose_landmarker_full.task")
+ if not os.path.isfile(model_path):
+ print("Downloading pose landmarker model...")
+ url = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/latest/pose_landmarker_full.task"
+ download_model(url, model_path)
+
+
+download_face_and_pose_landmarker()
+
paths = ["blender", "sam"]
files = []
diff --git a/blender/blender_node.py b/blender/blender_node.py
index cc3ed96..79cf0ca 100644
--- a/blender/blender_node.py
+++ b/blender/blender_node.py
@@ -100,14 +100,14 @@ class ObjectOps:
import global_bpy
bpy = global_bpy.get_bpy()
- if props.get("BPY_OBJ") != None:
+ if props.get("BPY_OBJ") is not None:
bpy.context.view_layer.objects.active = props["BPY_OBJ"]
results = self.blender_process(bpy, **props)
if results is None:
# print(results)
- if props.get("BPY_OBJ") != None:
+ if props.get("BPY_OBJ") is not None:
return (props["BPY_OBJ"], )
else:
return (bpy.context.view_layer.objects.active, )
diff --git a/blender/mesh_utils.py b/blender/mesh_utils.py
index 2dd6593..3b755e3 100644
--- a/blender/mesh_utils.py
+++ b/blender/mesh_utils.py
@@ -14,6 +14,9 @@ def genreate_mesh_from_texture(bpy, image):
contours, _ = cv2.findContours(
gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ if len(contours) == 0:
+ raise Exception("No contours found. Please ensure that the image has the correct segments (e.g. when you click on the mouth, it should display a proper blue area over the mouth region).")
+
# Get the largest contour
areas = [cv2.contourArea(contour) for contour in contours]
diff --git a/blender/ops_group.py b/blender/ops_group.py
index 211e8f3..45766f9 100644
--- a/blender/ops_group.py
+++ b/blender/ops_group.py
@@ -8,5 +8,6 @@ class GroupOps(blender_node.ObjectOps):
RETURN_TYPES = (blender_node.BPY_OBJS,)
def blender_process(self, bpy, BPY_OBJ, BPY_OBJ2, **props):
- return ([BPY_OBJ, BPY_OBJ2],)
+ prop_values = props.values()
+ return ([BPY_OBJ, BPY_OBJ2, *prop_values],)
diff --git a/blender/ops_join.py b/blender/ops_join.py
index 557e805..368761a 100644
--- a/blender/ops_join.py
+++ b/blender/ops_join.py
@@ -11,8 +11,10 @@ class Mesh_JoinMesh(blender_node.ObjectOps):
def blender_process(self, bpy, BPY_OBJ, **props):
prop_values = props.values()
for obj in list(prop_values) + [BPY_OBJ]:
- obj.select_set(True)
- bpy.context.view_layer.objects.active = BPY_OBJ
+ if obj is not None:
+ obj.select_set(True)
+ if bpy.context.view_layer.objects is not None:
+ bpy.context.view_layer.objects.active = BPY_OBJ
bpy.ops.object.join()
return (BPY_OBJ,)
diff --git a/blender/ops_modify_uv.py b/blender/ops_modify_uv.py
new file mode 100644
index 0000000..86d3e7c
--- /dev/null
+++ b/blender/ops_modify_uv.py
@@ -0,0 +1,24 @@
+import blender_node
+from mesh_utils import genreate_mesh_from_texture, assign_texture
+
+class Object_UV_Modifier(blender_node.EditOps):
+
+ EXTRA_INPUT_TYPES = {
+ "scale": ('FLOAT', {'default': 1, "display": "number", "step": 0.01}),
+ "texture_name": ('STRING', {'default': 'Texture', })
+ }
+
+ CUSTOM_NAME = "UV Modifier"
+
+ def blender_process(self, bpy, BPY_OBJ, scale, texture_name):
+ import bmesh
+
+ bm = bmesh.from_edit_mesh(BPY_OBJ.data)
+
+ uv_layer = bm.loops.layers.uv.verify()
+ for f in bm.faces:
+ # move all of the UVs in this face up one UDIM tile
+ for l in f.loops:
+ l[uv_layer].uv = (l[uv_layer].uv[0], 0.998 if l[uv_layer].uv[1] == 1 else l[uv_layer].uv[1])
+
+ bmesh.update_edit_mesh(BPY_OBJ.data)
\ No newline at end of file
diff --git a/js/Alert.js b/js/Alert.js
index aa477d1..a4c3f0d 100644
--- a/js/Alert.js
+++ b/js/Alert.js
@@ -3,8 +3,25 @@ import { van } from "./van.js";
const { div, span } = van.tags;
export function Alert() {
+ const color = van.state("bg-orange-100 text-orange-700 border-orange-500");
+
van.derive(() => {
if (alertDialog.val.time > 0) {
+ switch (alertDialog.val.type) {
+ case "error":
+ color.val = "bg-red-100 text-red-700 border-red-500";
+ break;
+ case "success":
+ color.val = "bg-green-100 text-green-700 border-green-500";
+ break;
+ case "info":
+ color.val = "bg-blue-100 text-blue-700 border-blue-500";
+ break;
+ case "warning":
+ default:
+ color.val = "bg-orange-100 text-orange-700 border-orange-500";
+ break;
+ }
setTimeout(() => {
alertDialog.val = { text: "", time: 0 };
}, alertDialog.val.time);
@@ -14,13 +31,12 @@ export function Alert() {
return div(
{
class: () =>
- "absolute bottom-8 flex justify-center w-full " +
+ "absolute z-[100] bottom-8 flex justify-center w-full " +
(alertDialog.val.text ? "" : "hidden"),
},
div(
{
- class:
- "bg-orange-100 border-t-4 border-orange-500 rounded-sm text-orange-700 p-2",
+ class: () => `${color.val} border-t-4 rounded-sm p-2`,
},
() => span(alertDialog.val.text)
)
diff --git a/js/AppHeader.js b/js/AppHeader.js
new file mode 100644
index 0000000..9026e0a
--- /dev/null
+++ b/js/AppHeader.js
@@ -0,0 +1,28 @@
+import { van } from "./van.js";
+const { div, span } = van.tags;
+
+export function AppHeader() {
+ return div(
+ {
+ class: () => "absolute flex justify-between top-0 w-full text-white p-4",
+ },
+ div(
+ {},
+ span(
+ {
+ class:
+ "block bg-gradient-to-b from-gray-500 to-white text-transparent bg-clip-text text-2xl",
+ },
+ "Avatech v1"
+ ),
+ span(
+ {
+ class:
+ "bg-gradient-to-b from-gray-500 to-white text-transparent bg-clip-text text-lg",
+ },
+ "Get your DALLE3 AI Personal Clone"
+ )
+ ),
+ span({ class: "text-gray-300" }, "Twitter")
+ );
+}
diff --git a/js/AvatarPreview.js b/js/AvatarPreview.js
index 1fe0e0a..d0a0f93 100644
--- a/js/AvatarPreview.js
+++ b/js/AvatarPreview.js
@@ -1,22 +1,817 @@
import { van } from "./van.js";
-const { button, iframe, div, img } = van.tags;
-import { showEditor, previewUrl, showPreview } from "./state.js";
+import {
+ imageUrl,
+ showPreview,
+ previewUrl,
+ showEditor,
+ previewImg,
+ previewImgLoading,
+ alertDialog,
+ isGenerateFlow,
+ enableAutoSegment
+} from "./state.js";
+const { button, iframe, div, img, input, label, span, textarea, ul, li } =
+ van.tags;
+import { app } from "./app.js";
+import { uploadPreview } from "./index.js";
+import { api } from "./api.js";
+import { segmented, uploadSegments } from "./LayerEditor.js";
+import { initModel } from "./onnx.js";
+// import { uploadSegments } from "./LayerEditor.js";
+
+const workflowList = [
+ "idle_avatar_(trigger)",
+ "Auto_segment_workflow",
+ "BronyaZaychik_(ChinaDress)",
+ "BronyaZaychik_(Default_Silverwing)",
+ "BronyaZaychik_(Non-official_office_ladysuit)",
+ "BronyaZaychik_(Official_office_ladysuit)",
+ "BronyaZaychikLora_withhand",
+ "SilverWolf_(Default)",
+ "SilverWolf_(Maid)",
+ "SilverWolfLora_withhand",
+];
+
+function editSegment(stage) {
+ /** @type {import('../../../web/types/litegraph.js').LGraph}*/
+ const graph = app.graph;
+ const imageNodes = graph.findNodesByType("LoadImage");
+ if (!imageNodes[0].imgs) return;
+
+ const nodes = graph.findNodesByType("SAM MultiLayer");
+
+ /** @type {any[]}*/
+ const widgets = nodes[0].widgets;
+ console.log(nodes[0]);
+ console.log(nodes[0].widgets);
+ widgets.find((x) => x.type == "button").callback();
+ stage.val = 2;
+}
+
+// const workflowList = ["Auto_segment_workflow"];
+/**
+ * Load JSON workflow
+ * @param {string} name - The name of the workflow to load
+ */
+async function loadJSONWorkflow(name) {
+ if (name === 'default' || name.toLowerCase().startsWith("auto_segment")) {
+ enableAutoSegment.val = true
+ } else {
+ enableAutoSegment.val = false
+ }
+ const json = await (await fetch(`./get_workflow?name=${name}`)).json();
+ app.loadGraphData(json);
+ console.log(json);
+}
+
+async function updatePositivePrompt(app, prompt) {
+ const positivePrompt = app.graph
+ .findNodesByType("CLIPTextEncode")
+ .find((x) => x.color == "#232");
+ if (!positivePrompt) {
+ alertDialog.val = {
+ text: "Cannot find the CLIPTextEncode node. Please make sure the workflow is correct.",
+ time: 5000,
+ };
+ return;
+ }
+
+ positivePrompt.widgets[0].inputEl.value = prompt;
+}
+
+async function updateSeedValue(app, seed) {
+ const kSampler = app.graph.findNodesByType("KSampler")[0];
+ if (!kSampler) {
+ alertDialog.val = {
+ text: "Cannot find the KSampler node. Please make sure the workflow is correct.",
+ time: 5000,
+ };
+ return;
+ }
+ kSampler.widgets[0].value = seed;
+ kSampler.widgets[1].value = "fixed";
+}
+
+async function uploadImage() {
+ /** @type {import('../../../web/types/litegraph.js').LGraph}*/
+ const graph = app.graph;
+ const nodes = graph.findNodesByType("LoadImage");
+ previewImgLoading.val = true;
+ console.log(previewImgLoading.val);
+
+ /** @type {any[]}*/
+ const widgets = nodes[0].widgets;
+ console.log(nodes[0]);
+ widgets.find((x) => x.type == "button").callback();
+ while (true) {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ if (nodes[0]?.imgs) {
+ if (previewImg.val != "" && previewImg.val == nodes[0].imgs[0].currentSrc)
+ continue;
+ previewImgLoading.val = false;
+ return nodes[0].imgs[0].currentSrc;
+ }
+ }
+}
+
+const jsonWorkflowLoading = van.state(true);
+export const sharedAvatarLink = van.state("");
+
+async function prepareImageFromUrlRedirect(stage) {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ const queue_id = new URLSearchParams(window.location.search).get("queue-id");
+ if (queue_id && queue_id != "") {
+ console.log(queue_id);
+ stage.val = 1;
+ const graph = app.graph;
+ const node = graph.findNodesByType("LoadImage");
+ const imageName = queue_id + ".png";
+ console.log(node[0]);
+ node[0].widgets_values[0] = imageName;
+ node[0].widgets[0].value = imageName;
+ node[0].widgets[0]._value = imageName;
+ graph.change();
+ previewImg.val = api.apiURL(
+ `/view?filename=${encodeURIComponent(
+ imageName
+ )}&type=input&subfolder=create_avatar_endpoint${app.getPreviewFormatParam()}`
+ );
+ console.log(previewImg);
+ }
+ const dragndrop = document.getElementById("dnd");
+ dragndrop.addEventListener("dragenter", (evt) => {
+ evt.preventDefault();
+ dragndrop.className =
+ "h-96 w-full border-2 border-purple-500 text-purple-500 border-dashed rounded-lg flex justify-center items-center";
+ });
+ dragndrop.addEventListener("dragleave", (evt) => {
+ evt.preventDefault();
+ dragndrop.className =
+ "h-96 w-full border-2 border-black border-dashed items-center rounded-lg flex justify-center";
+ });
+ dragndrop.addEventListener("dragover", (evt) => {
+ evt.preventDefault();
+ });
+ dragndrop.addEventListener("drop", async (evt) => {
+ evt.preventDefault();
+ dragndrop.className =
+ "h-96 w-full border-2 border-black border-dashed items-center rounded-lg flex justify-center";
+ if (evt.dataTransfer.files.length > 1) return;
+ if (
+ evt.dataTransfer.files[0].type != "image/jpeg" &&
+ evt.dataTransfer.files[0].type != "image/png" &&
+ evt.dataTransfer.files[0].type != "image/webp"
+ )
+ return;
+ stage.val = 1;
+ previewImg.val = URL.createObjectURL(evt.dataTransfer.files[0]);
+ if (Object.entries(evt.dataTransfer.files).length) {
+ await uploadFile(evt.dataTransfer.files[0], true);
+ }
+ });
+}
export function AvatarPreview() {
+ console.log("getting workflow json now");
+ loadJSONWorkflow("default").then(() => {
+ console.log("done loading");
+ jsonWorkflowLoading.val = false;
+ });
- return div(
- { class: () => (!showEditor.val ? "" : "hidden") },
- iframe({
+ const loading = van.state(false);
+ const shareLoading = van.state("share"); // share, loading, shared
+
+ api.addEventListener("execution_start", (evt) => {
+ loading.val = true;
+ });
+
+ api.addEventListener("executed", (evt) => {
+ const nodeId = evt.detail.node;
+ const targetNode = graph._nodes_by_id[nodeId];
+ if (targetNode.type === "AvatarMainOutput") {
+ loading.val = false;
+ }
+ });
+
+ const email = van.state("");
+ const stage = van.state(0); // 0: upload image, 1: edit segment, 2: generate
+
+ // This will wait 2 seconds until the everything is loaded
+ prepareImageFromUrlRedirect(stage);
+
+ const renderSteps = () => {
+ return div(
+ {
+ class: () =>
+ "flex flex-col bg-white justify-center w-[32rem] max-w-[100%]",
+ },
+ div(
+ {
+ class: () =>
+ " bg-gradient-to-b from-black via-[#5F5F5F] via-60% to-white text-transparent bg-clip-text font-gabarito text-4xl",
+ },
+ "Avatech v1"
+ ),
+ div(
+ {
+ class: () =>
+ " bg-gradient-to-b from-black via-[#5F5F5F] via-50% to-white text-transparent bg-clip-text font-gabarito text-2xl",
+ },
+ "Get your DALLE3 AI Personal Clone"
+ ),
+ div(
+ {
+ class: () =>
+ " w-full flex flex-col justify-center items-center gap-4",
+ },
+ !isGenerateFlow.val
+ ? div(
+ {
+ class: () =>
+ "flex flex-col justify-center items-center gap-4 w-full",
+ },
+ div(
+ { class: () => "w-full flex mt-2" },
+ button(
+ {
+ class: () => `btn w-full normal-case`,
+ onclick: async () => {
+ // previewImg.val = await uploadImage();
+ // stage.val = 1;
+ var input = document.createElement("input");
+ input.type = "file";
+
+ document.body.appendChild(input);
+
+ // when the input content changes, do something
+ input.onchange = async function (e) {
+ stage.val = 1;
+ if (Object.entries(e.target.files).length) {
+ await uploadFile(e.target.files[0], true);
+ }
+ previewImg.val = URL.createObjectURL(e.target.files[0]);
+ // upload files
+ document.body.removeChild(input);
+ };
+
+ // Trigger file browser
+ input.click();
+ },
+ },
+ div({ class: "badge badge-neutral" }, "1"),
+ div("Upload your image"),
+ span({
+ class: "iconify text-lg",
+ "data-icon": "material-symbols:drive-folder-upload",
+ "data-inline": "false",
+ }),
+ () =>
+ previewImgLoading.val
+ ? span({
+ class: "loading loading-spinner loading-md",
+ })
+ : "",
+ ),
+ ),
+ () => {
+ const dnd = div(
+ {
+ id: "dnd",
+ class: () =>
+ "h-96 w-full border-2 border-black border-dashed items-center rounded-lg flex justify-center text-black",
+ },
+ "or drag and drop the image here",
+ );
+ const image = img({
+ class: () => "z-[10] object-contain w-full h-[394px] border",
+ src: previewImg,
+ onload: () => {
+ segmented.val = false;
+ }
+ });
+
+ if (isMobileDevice()) {
+ return previewImg.val !== "" ? image : "";
+ } else {
+ return previewImg.val === "" ? dnd : image;
+ }
+ },
+ button(
+ {
+ class: () =>
+ "btn w-full normal-case " +
+ (stage.val < 1 ? "btn-disabled" : ""),
+ onclick: () => {
+ enableAutoSegment.val = true;
+ editSegment(stage)
+ },
+ },
+ div({ class: "badge badge-neutral" }, "2"),
+ "Edit Segment",
+ ),
+ button(
+ {
+ class: () =>
+ "btn w-full normal-case " +
+ (stage.val < 2 ? "btn-disabled" : ""),
+ onclick: async () => {
+ // const uploaded = await uploadSegments();
+ // if (!uploaded) return;
+
+ const graph = app.graph;
+ const imageNodes = graph.findNodesByType("LoadImage");
+ if (!imageNodes[0].imgs) return;
+ document.getElementById("queue-button").click();
+ },
+ },
+ div({ class: "badge badge-neutral" }, "3"),
+ () =>
+ loading.val
+ ? span({
+ class: "loading loading-spinner loading-md",
+ })
+ : "Make It Alive!",
+ ),
+ )
+ : div(
+ {
+ class:
+ "flex flex-col justify-center items-center gap-4 w-full text-black",
+ },
+ div(
+ {
+ class:
+ "w-full mt-2 flex flex-col rounded-md left-0 top-0",
+ },
+ textarea({
+ class:
+ "textarea textarea-bordered border-gray-300 border-b-0 focus:outline-none resize-none rounded-t-md rounded-b-none text-md h-36",
+ placeholder: "Enter your prompt",
+ defaultValue:
+ "1girl, looking at viewer, open mouth, simple background, white background, smile",
+ id: "positivePromptProxy",
+ }),
+ div(
+ {
+ class:
+ "flex flex-row gap-2 border border-gray-300 rounded-b-md text-md items-center",
+ },
+ span({ class: "ml-4" }, "Seed"),
+ div({ class: "divider divider-horizontal m-0" }),
+ input({
+ type: "text",
+ class: "input border-none focus:outline-none w-full p-0",
+ placeholder: "Seed",
+ defaultValue: "1234",
+ id: "seedProxy",
+ }),
+ div(
+ {
+ onclick: () => {
+ const random4Digits =
+ Math.floor(Math.random() * 9000) + 1000;
+ console.log(
+ random4Digits,
+ document.getElementById("seedProxy").value,
+ );
+ document.getElementById("seedProxy").value =
+ random4Digits.toString();
+ },
+ },
+ span({
+ class: "iconify text-2xl mr-4 hover:cursor-pointer",
+ "data-icon": "fad:random-1dice",
+ "data-inline": "false",
+ }),
+ ),
+ ),
+ ),
+ button(
+ {
+ class: "btn w-full normal-case ",
+ onclick: async () => {
+ loading.val = true;
+
+ updatePositivePrompt(
+ app,
+ document.getElementById("positivePromptProxy").value,
+ );
+ updateSeedValue(
+ app,
+ document.getElementById("seedProxy").value,
+ );
+
+ const sam = app.graph.findNodesByType("SAM MultiLayer")[0];
+ if (!sam) {
+ alertDialog.val = {
+ text: "Cannot find the SAM node. Please make sure the workflow is correct.",
+ time: 5000,
+ };
+ return;
+ }
+ const ckpt = sam.widgets[0].value;
+ const modelType = ckpt.match(/vit_[lbh]/)?.[0];
+ await initModel(modelType);
+ await uploadSegments();
+
+ document.getElementById("queue-button").click();
+ },
+ },
+ div({ class: "badge badge-neutral" }, "1"),
+ () =>
+ loading.val
+ ? span({ class: "loading loading-spinner loading-md" })
+ : "Make It Alive!",
+ ),
+ button(
+ {
+ class: () =>
+ "btn w-full normal-case ",
+ onclick: () => {
+ enableAutoSegment.val = false;
+ editSegment(stage)
+ },
+ },
+ div({ class: "badge badge-neutral" }, "2"),
+ "Edit Segment",
+ ),
+ // button(
+ // {
+ // class: "btn w-full normal-case",
+ // onclick: () => {
+ // /** @type {import('../../../web/types/litegraph.js').LGraph}*/
+ // const graph = app.graph;
+ // const nodes = graph.findNodesByType("SAM MultiLayer");
+
+ // /** @type {any[]}*/
+ // const widgets = nodes[0].widgets;
+ // console.log(nodes[0]);
+ // console.log(nodes[0].widgets);
+ // widgets.find((x) => x.type == "button").callback();
+ // },
+ // },
+ // div({ class: "badge badge-neutral" }, "2"),
+ // "(Optional) Edit Segment",
+ // ),
+ ),
+ ),
+ );
+ };
+
+ const renderIFrame = () => {
+ return iframe({
id: "avatech-viewer-iframe",
title: "avatech-viewer-iframe",
name: "avatech-viewer-iframe",
allow: "cross-origin-isolated",
class: () =>
- "w-[320px] h-[370px] absolute right-0 top-0 z-[100] pointer-events-auto flex mt-4 mr-4 rounded-2xl border-none " +
+ "w-full h-full min-w-[400px] min-h-[400px] z-[100] pointer-events-auto flex border-none overflow-hidden" +
(showPreview.val ? "" : "hidden"),
// src: "https://labs.avatech.ai/viewer/default",
// src: "http://localhost:3000/viewer/default",
src: previewUrl,
- })
+ });
+ };
+
+ const renderShareLink = () => {
+ return div(
+ {
+ class: () =>
+ "w-full flex flex-col gap-2 justify-center items-center mt-8",
+ },
+ div(
+ {
+ class: () =>
+ "w-full flex justify-center font-bold italic text-gray-500",
+ },
+ span("We are launching OpenAI Assistant API integration soon!")
+ ),
+ div(
+ { class: () => "w-[24rem] flex justify-center items-center" },
+ input({
+ type: "text",
+ class: () =>
+ "w-full input input-bordered text-black rounded rounded-l-md rounded-r-none !outline-none",
+ onchange: (e) => {
+ email.val = e.target.value;
+ },
+ placeholder: "Enter your email",
+ }),
+ button(
+ {
+ class: () =>
+ "btn rounded rounded-l-none rounded-r-md no-animation bg-neutral-800 hover:bg-neutral-950 text-white border-none normal-case",
+ onclick: async () => {
+ if (shareLoading.val === "share") {
+ shareLoading.val = "loading";
+ const url = await (await fetch("./get_webhook")).json();
+ await uploadPreview();
+ await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ username: "Avabot",
+ avatar_url:
+ "https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/avatechai.png",
+ content: "New register! \n" + email.val,
+ }),
+ });
+ shareLoading.val = "shared";
+ }
+ if (sharedAvatarLink.val) {
+ await navigator.clipboard.writeText(sharedAvatarLink.val);
+ alertDialog.val = {
+ text: "Avatar link copied to clipboard!",
+ type: "success",
+ time: 5000,
+ };
+ }
+ },
+ },
+ () => {
+ switch (shareLoading.val) {
+ case "share":
+ return "Get Avatar Link";
+ case "loading":
+ return span({
+ class: "loading loading-spinner loading-md",
+ });
+ case "shared":
+ return span({
+ class: "iconify text-xl",
+ "data-icon": "lucide:copy-check",
+ });
+ }
+ }
+ )
+ )
+ );
+ };
+
+ const renderCloseButton = () => {
+ return button(
+ {
+ class: () =>
+ "btn flex flex-row btn-ghost text-black normal-case rounded-md left-0 top-0 z-[200] pointer-events-auto sm:btn-md btn-sm ",
+ onclick: () => {
+ showPreview.val = false;
+ },
+ },
+ span({
+ class: "iconify text-lg",
+ "data-icon": "ic:round-close",
+ "data-inline": "false",
+ })
+ );
+ };
+
+ const renderRestartButton = () => {
+ return button(
+ {
+ class: () =>
+ "btn flex flex-row btn-ghost text-black normal-case rounded-md left-0 top-0 z-[200] pointer-events-auto sm:btn-md btn-sm ",
+ onclick: () => {
+ fetch("https://7a49f4ad27be4dcf.ngrok.app/restart");
+ },
+ },
+ span({
+ class: "iconify text-lg",
+ "data-icon": "mdi:restart",
+ "data-inline": "false",
+ })
+ );
+ };
+
+ const renderChangeWorkflowButton = () => {
+ return div(
+ {
+ class: () =>
+ "dropdown dropdown-hover dropdown-bottom z-[200] pointer-events-auto text-black ",
+ },
+ label(
+ {
+ class: () =>
+ "btn flex flex-row btn-ghost normal-case rounded-md sm:btn-md btn-sm",
+ tabIndex: () => 0,
+ },
+ span({
+ class: "iconify text-lg",
+ "data-icon": "ic:round-swap-vert",
+ "data-inline": "false",
+ }),
+ span({ class: "sm:flex hidden" }, () =>
+ jsonWorkflowLoading.val ? "Loading" : "Change workflow"
+ )
+ ),
+ ul(
+ {
+ class: () =>
+ "dropdown-content -left-[100px] z-[200] menu p-2 shadow rounded-box w-96 bg-white",
+ tabIndex: () => 0,
+ },
+ workflowList.map((val, index) => {
+ return li(
+ {
+ class: () => "p-4 btn btn-ghost items-start",
+ onclick: async (e) => {
+ e.preventDefault();
+ document.activeElement.blur()
+ await loadJSONWorkflow(val);
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ const kSampler = app.graph.findNodesByType("KSampler")[0];
+ if (!kSampler) isGenerateFlow.val = false;
+ else isGenerateFlow.val = true;
+ },
+ },
+ () => val
+ );
+ }),
+ div({ class: () => "divider !my-0" }),
+ li(
+ {
+ class: () => "p-4 btn btn-ghost items-start",
+ onclick: (e) => {
+ let input = document.createElement("input");
+ input.type = "file";
+ document.body.appendChild(input);
+ input.accept = ".json,image/png,.latent,.safetensors";
+ input.onchange = async function (e) {
+ if (Object.entries(e.target.files).length) {
+ await app.handleFile(e.target.files[0]);
+ }
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ const kSampler = app.graph.findNodesByType("KSampler")[0];
+ if (!kSampler) isGenerateFlow.val = false;
+ else isGenerateFlow.val = true;
+ document.body.removeChild(input);
+ };
+ input.click();
+ // document.getElementById("comfy-load-button").click();
+ },
+ },
+ "Import..."
+ )
+ )
+ );
+ // return button(
+ // {
+ // class: () =>
+ // "btn text-black flex flex-row btn-ghost normal-case rounded-md left-0 top-0 z-[200] pointer-events-auto sm:btn-md btn-sm ",
+ // onclick: () => {
+ // let input = document.createElement("input");
+ // input.type = "file";
+ // document.body.appendChild(input);
+ // input.accept = ".json,image/png,.latent,.safetensors";
+ // input.onchange = async function (e) {
+ // if (Object.entries(e.target.files).length) {
+ // await app.handleFile(e.target.files[0]);
+ // }
+ // await new Promise((resolve) => setTimeout(resolve, 200));
+ // const kSampler = app.graph.findNodesByType("KSampler")[0];
+ // if (!kSampler) isGenerateFlow.val = false;
+ // else isGenerateFlow.val = true;
+ // document.body.removeChild(input);
+ // };
+ // input.click();
+ // // document.getElementById("comfy-load-button").click();
+ // },
+ // },
+ // span({
+ // class: "iconify text-lg",
+ // "data-icon": "ic:round-swap-vert",
+ // "data-inline": "false",
+ // }),
+ // span({ class: "sm:flex hidden" }, () =>
+ // jsonWorkflowLoading.val ? "Loading" : "Change workflow",
+ // ),
+ // );
+ };
+
+ const renderTwitter = () => {
+ return button(
+ {
+ class: () =>
+ "absolute top-4 right-4 btn sm:w-32 w-20 text-black btn-ghost text-xs z-[200] !px-0 normal-case sm:btn-md btn-sm",
+ onclick: () => window.open("https://twitter.com/avatech_gg", "_blank"),
+ },
+ "Twitter"
+ );
+ };
+
+ const isMobileDevice = () => {
+ return window.screen.width < 768;
+ };
+
+ return div(
+ {
+ class: () => {
+ console.log(showPreview);
+
+ return (
+ (showPreview.val ? "" : "hidden ") +
+ "w-full h-full absolute left-0 top-0 z-[99] pointer-events-auto flex border-none bg-white"
+ );
+ },
+ },
+ div(
+ { class: "overflow-y-auto overflow-x-hidden w-full h-full" },
+ div(
+ {
+ class: "absolute top-4 left-4 flex flex-row gap-2",
+ },
+ renderCloseButton(),
+ renderRestartButton(),
+ renderChangeWorkflowButton()
+ ),
+ renderTwitter(),
+ () => {
+ if (isMobileDevice()) {
+ return div(
+ {
+ class: () =>
+ "flex flex-col w-full h-fit bg-white justify-center items-center py-16 px-4 gap-2" +
+ (showPreview.val ? "" : "hidden"),
+ },
+ renderIFrame(),
+ renderSteps(),
+ renderShareLink()
+ );
+ } else {
+ return div(
+ {
+ class: () =>
+ "flex w-full h-full bg-white justify-around items-center p-24" +
+ (showPreview.val ? "" : "hidden"),
+ },
+ renderSteps(),
+ div(
+ { class: () => "flex flex-col" },
+ renderIFrame(),
+ renderShareLink()
+ )
+ );
+ }
+ }
+ )
);
}
+
+function showImage(name) {
+ const graph = app.graph;
+ const node = graph.findNodesByType("LoadImage");
+ const img = new Image();
+ img.onload = () => {
+ node[0].imgs = [img];
+ app.graph.setDirtyCanvas(true);
+ };
+ let folder_separator = name.lastIndexOf("/");
+ let subfolder = "";
+ if (folder_separator > -1) {
+ subfolder = name.substring(0, folder_separator);
+ name = name.substring(folder_separator + 1);
+ }
+ img.src = api.apiURL(
+ `/view?filename=${encodeURIComponent(
+ name
+ )}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`
+ );
+ node.setSizeForImage?.();
+}
+
+async function uploadFile(file, updateNode, pasted = false) {
+ try {
+ // Wrap file in formdata so it includes filename
+ const graph = app.graph;
+ const nodes = graph.findNodesByType("LoadImage");
+ const widgets = nodes[0].widgets.find((w) => w.name === "image");
+ const body = new FormData();
+ body.append("image", file);
+ if (pasted) body.append("subfolder", "pasted");
+ const resp = await api.fetchApi("/upload/image", {
+ method: "POST",
+ body,
+ });
+
+ if (resp.status === 200) {
+ const data = await resp.json();
+ // Add the file to the dropdown list and update the widget value
+ let path = data.name;
+ if (data.subfolder) path = data.subfolder + "/" + path;
+
+ if (!widgets.options.values.includes(path)) {
+ widgets.options.values.push(path);
+ }
+
+ if (updateNode) {
+ showImage(path);
+ widgets.value = path;
+ }
+ } else {
+ alert(resp.status + " - " + resp.statusText);
+ }
+ } catch (error) {
+ alert(error);
+ }
+}
diff --git a/js/Container.js b/js/Container.js
index 6f28cde..8130d57 100644
--- a/js/Container.js
+++ b/js/Container.js
@@ -4,6 +4,7 @@ import { van } from './van.js';
import { AvatarPreview } from './AvatarPreview.js';
import { Loading } from './Loading.js';
import { Alert } from './Alert.js';
+import { AppHeader } from './AppHeader.js';
const { button, iframe, div, img } = van.tags;
export function Container() {
@@ -16,6 +17,6 @@ export function Container() {
LayerEditor(),
AvatarPreview(),
Loading(),
- Alert(),
+ Alert()
);
}
diff --git a/js/GetShareLink.js b/js/GetShareLink.js
new file mode 100644
index 0000000..d35db18
--- /dev/null
+++ b/js/GetShareLink.js
@@ -0,0 +1,29 @@
+import { van } from "./van.js";
+const { button, div, span, input } = van.tags;
+
+export function GetShareLink() {
+ return div(
+ {
+ class: () =>
+ "absolute flex flex-col justify-center items-center top-0 left-0 bg-gray-900 bg-opacity-50 pointer-events-auto w-full h-full gap-2",
+ },
+ span("We're launching OpenAI Assistant API integration soon!"),
+ div(
+ {
+ class: "w-[24rem] flex justify-center items-center",
+ },
+ input({
+ class:
+ "w-full input input-bordered text-black rounded rounded-l-md rounded-r-none",
+ placeholder: "Email",
+ }),
+ button(
+ {
+ class:
+ "btn rounded rounded-l-none rounded-r-md no-animation bg-neutral hover:bg-neutral-focus text-white border-none normal-case",
+ },
+ "Get Avatar Link"
+ )
+ )
+ );
+}
diff --git a/js/LayerEditor.js b/js/LayerEditor.js
index 46ae696..41fe2f1 100644
--- a/js/LayerEditor.js
+++ b/js/LayerEditor.js
@@ -1,5 +1,6 @@
import { SideBar } from "./SideBar.js";
import { api } from "./api.js";
+import { app } from "./app.js";
import { runONNX } from "./onnx.js";
import {
showImageEditor,
@@ -13,11 +14,294 @@ import {
imagePromptsMulti,
embeddings,
embeddingID,
+ alertDialog,
+ allImagePrompts,
+ boxesMulti,
+ enableAutoSegment,
} from "./state.js";
import { van } from "./van.js";
+import vision from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3";
+const { PoseLandmarker, FaceLandmarker, FilesetResolver } = vision;
const { button, div, img, canvas, span } = van.tags;
let throttle = false;
+const positivePrompt = van.state(true);
+const enableBackgroundRemover = van.state(true);
+const isMobileDevice = () => {
+ return window.screen.width < 768;
+};
+
+// Auto segmentation
+const filesetResolver = await FilesetResolver.forVisionTasks(
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm"
+);
+const faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, {
+ baseOptions: {
+ modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task`,
+ delegate: "GPU",
+ },
+ // outputFaceBlendshapes: true,
+ runningMode: "IMAGE",
+ numFaces: 1,
+});
+const poseLandmarker = await PoseLandmarker.createFromOptions(filesetResolver, {
+ baseOptions: {
+ modelAssetPath: `https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task`,
+ delegate: "GPU",
+ },
+ runningMode: "IMAGE",
+ numPoses: 1,
+});
+const layerMapping = {
+ L_eye: {
+ useMiddle: false,
+ positiveOffsetX: 0,
+ positiveOffsetY: 0,
+ negativeOffsetX: 0,
+ negativeOffsetY: 0,
+ positiveScale: 0.25,
+ negativeScale: 0.5,
+ indices: FaceLandmarker.FACE_LANDMARKS_LEFT_EYE,
+ },
+ R_eye: {
+ useMiddle: false,
+ positiveOffsetX: 0,
+ positiveOffsetY: 0,
+ negativeOffsetX: 0,
+ negativeOffsetY: 0,
+ positiveScale: 0.25,
+ negativeScale: 0.5,
+ indices: FaceLandmarker.FACE_LANDMARKS_RIGHT_EYE,
+ },
+ L_iris: {
+ useMiddle: false,
+ positiveOffsetX: 0,
+ positiveOffsetY: 0,
+ negativeOffsetX: 0,
+ negativeOffsetY: 0,
+ positiveScale: -0.2,
+ negativeScale: 0.5,
+ indices: FaceLandmarker.FACE_LANDMARKS_LEFT_IRIS,
+ },
+ R_iris: {
+ useMiddle: false,
+ positiveOffsetX: 0,
+ positiveOffsetY: 0,
+ negativeOffsetX: 0,
+ negativeOffsetY: 0,
+ positiveScale: -0.2,
+ negativeScale: 0.5,
+ indices: FaceLandmarker.FACE_LANDMARKS_RIGHT_IRIS,
+ },
+ face: {
+ useMiddle: false,
+ positiveOffsetX: 0,
+ positiveOffsetY: 60,
+ negativeOffsetX: 0,
+ negativeOffsetY: 0,
+ positiveScale: 0.5,
+ negativeScale: 0,
+ indices: FaceLandmarker.FACE_LANDMARKS_FACE_OVAL,
+ },
+ mouth: {
+ useMiddle: false,
+ positiveOffsetX: 0,
+ positiveOffsetY: 0,
+ negativeOffsetX: 0,
+ negativeOffsetY: 0,
+ positiveScale: -0.3,
+ negativeScale: 0.3,
+ // https://stackoverflow.com/questions/66649492/how-to-get-specific-landmark-of-face-like-lips-or-eyes-using-tensorflow-js-face
+ indices: [61, 37, 270, 91, 314].map((x) => ({
+ start: x,
+ end: x,
+ })),
+ },
+ mouth_in: {
+ useMiddle: false,
+ positiveOffsetX: 0,
+ positiveOffsetY: 0,
+ negativeOffsetX: 0,
+ negativeOffsetY: 0,
+ positiveScale: -0.5,
+ negativeScale: 0.5,
+ // https://stackoverflow.com/questions/66649492/how-to-get-specific-landmark-of-face-like-lips-or-eyes-using-tensorflow-js-face
+ indices: [310, 88].map((x) => ({
+ start: x,
+ end: x,
+ })),
+ },
+};
+
+export const segmented = van.state(false);
+
+export async function autoSegment() {
+ const image = document.getElementById("image");
+ const landmarks = faceLandmarker.detect(image).faceLandmarks[0];
+
+ Object.entries(layerMapping).forEach(([key, value]) => {
+ imagePromptsMulti.val[key] = [];
+ });
+
+ Object.entries(layerMapping).forEach(([key, value]) => {
+ const positivePoints = [];
+ const middlePoints = [];
+ const negativePoints = [];
+
+ // Positive points
+ for (const { start, end } of value.indices) {
+ const startPoint = landmarks[start];
+ // const endPoint = landmarks[end];
+
+ const startX = startPoint.x * imageSize.val.width;
+ const startY = startPoint.y * imageSize.val.height;
+
+ // const endX = endPoint.x * imageSize.val.width;
+ // const endY = endPoint.y * imageSize.val.height;
+
+ if (middlePoints.length === 0) {
+ middlePoints.push({ x: startX, y: startY, label: 1, isAuto: true });
+ // middlePoints.push({ x: endX, y: endY, label: 1 });
+ } else {
+ middlePoints[0].x += startX;
+ middlePoints[0].y += startY;
+ // middlePoints[1].x += endX;
+ // middlePoints[1].y += endY;
+ }
+ positivePoints.push({ x: startX, y: startY, label: 1, isAuto: true });
+ // positivePoints.push({ x: endX, y: endY, label: 1 });
+
+ // imagePrompts.val = [...imagePrompts.val, { x, y, label: 1 }];
+ }
+
+ // Middle points
+ const len = value.indices.length;
+ middlePoints[0].x /= len;
+ middlePoints[0].y /= len;
+ // middlePoints[1].x /= len;
+ // middlePoints[1].y /= len;
+
+ if (value.useMiddle) {
+ imagePromptsMulti.val[key] = [
+ ...imagePromptsMulti.val[key],
+ ...middlePoints,
+ ];
+ } else {
+ // Negative points
+ for (const [i, { start, end }] of value.indices.entries()) {
+ const startPoint = landmarks[start];
+ // const endPoint = landmarks[end];
+
+ const startX = startPoint.x * imageSize.val.width;
+ const startY = startPoint.y * imageSize.val.height;
+
+ // const endX = endPoint.x * imageSize.val.width;
+ // const endY = endPoint.y * imageSize.val.height;
+
+ const middlePoint = middlePoints[0];
+ const directionVector = {
+ x: middlePoint.x - startX,
+ y: middlePoint.y - startY,
+ };
+ const directionVectorLength = Math.sqrt(
+ directionVector.x * directionVector.x +
+ directionVector.y * directionVector.y
+ );
+
+ if (value.negativeScale !== 0) {
+ const negativePointDistance =
+ value.negativeScale * directionVectorLength;
+ const negativePoint = {
+ x:
+ startX -
+ (negativePointDistance * directionVector.x) /
+ directionVectorLength -
+ value.negativeOffsetX,
+ y:
+ startY -
+ (negativePointDistance * directionVector.y) /
+ directionVectorLength -
+ value.negativeOffsetY,
+ label: 0,
+ isAuto: true,
+ };
+ negativePoints.push(negativePoint);
+ }
+
+ const positivePointDistance =
+ value.positiveScale * directionVectorLength;
+ positivePoints[i] = {
+ x:
+ positivePoints[i].x -
+ (positivePointDistance * directionVector.x) /
+ directionVectorLength -
+ value.positiveOffsetX,
+ y:
+ positivePoints[i].y -
+ (positivePointDistance * directionVector.y) /
+ directionVectorLength -
+ value.positiveOffsetY,
+ label: 1,
+ isAuto: true,
+ };
+ }
+ imagePromptsMulti.val[key] = [
+ ...imagePromptsMulti.val[key],
+ ...positivePoints,
+ ...negativePoints,
+ ];
+ }
+
+ // Find bounding box of positive/negative points
+ const points = negativePoints.length > 0 ? negativePoints : positivePoints;
+ const box = {
+ x1: Math.min(...points.map((x) => x.x)),
+ y1: Math.min(...points.map((x) => x.y)),
+ x2: Math.max(...points.map((x) => x.x)),
+ y2: Math.max(...points.map((x) => x.y)),
+ };
+ boxesMulti.val[key] = box;
+ });
+
+ const poseLandmarks = poseLandmarker.detect(image).landmarks[0];
+ const positiveBreathX =
+ ((poseLandmarks[11].x + poseLandmarks[12].x) / 2) * imageSize.val.width;
+ const positiveBreathY =
+ ((poseLandmarks[11].y + poseLandmarks[12].y) / 2) * imageSize.val.height;
+ const negativeBreathX1 = poseLandmarks[0].x * imageSize.val.width;
+ const negativeBreathY1 = poseLandmarks[0].y * imageSize.val.height;
+ const negativeBreathX2 = poseLandmarks[9].x * imageSize.val.width;
+ const negativeBreathY2 = poseLandmarks[9].y * imageSize.val.height;
+ const negativeBreathX3 = poseLandmarks[10].x * imageSize.val.width;
+ const negativeBreathY3 = poseLandmarks[10].y * imageSize.val.height;
+
+ imagePromptsMulti.val["breath"] = [
+ { x: positiveBreathX, y: positiveBreathY, label: 1, isAuto: true },
+ { x: negativeBreathX1, y: negativeBreathY1, label: 0, isAuto: true },
+ { x: negativeBreathX2, y: negativeBreathY2, label: 0, isAuto: true },
+ { x: negativeBreathX3, y: negativeBreathY3, label: 0, isAuto: true },
+ ];
+ imagePrompts.val = imagePromptsMulti.val[selectedLayer.val];
+ segmented.val = true;
+ console.log("Done");
+}
+
+export function setRemoveBackgroundNode() {
+ const rmBgNodes = app.graph.findNodesByType(
+ "Image Rembg (Remove Background)"
+ );
+ if (!rmBgNodes?.length) {
+ alertDialog.val = {
+ text: "Remove background node not found. Please ensure the workflow is correct.",
+ time: 5000,
+ };
+ return;
+ }
+ rmBgNodes.forEach((node) => {
+ // node is bypassed if mode is 4
+ node.mode = enableBackgroundRemover.val ? 0 : 4;
+ });
+}
export function updateImagePrompts() {
if (selectedLayer.val !== "" && selectedLayer.val !== undefined) {
@@ -29,17 +313,17 @@ export function updateImagePrompts() {
targetNode.val.widgets.find((x) => x.name === "image_prompts_json").value =
JSON.stringify(imagePromptsMulti.val);
- const canvas = document.getElementById("mask-canvas");
- const base64Image = canvas.toDataURL();
- api.fetchApi("/segments", {
- method: "POST",
- body: JSON.stringify({
- name: embeddingID.val,
- segments: {
- [selectedLayer.val]: base64Image,
- },
- }),
- });
+ // const canvas = document.getElementById("mask-canvas");
+ // const base64Image = canvas.toDataURL();
+ // api.fetchApi("/segments", {
+ // method: "POST",
+ // body: JSON.stringify({
+ // name: embeddingID.val,
+ // segments: {
+ // [selectedLayer.val]: base64Image,
+ // },
+ // }),
+ // });
} else {
targetNode.val.widgets.find((x) => x.name === "image_prompts_json").value =
JSON.stringify(imagePrompts.val);
@@ -47,6 +331,43 @@ export function updateImagePrompts() {
targetNode.val.graph.change();
}
+export async function uploadSegments() {
+ const emptyLayers = [];
+ Object.entries(imagePromptsMulti.val).forEach(([key, value]) => {
+ if (value.length === 0) {
+ emptyLayers.push(key);
+ }
+ });
+ if (emptyLayers.length > 0) {
+ alertDialog.val = {
+ text: "The following layers have no segments: " + emptyLayers.join(", "),
+ time: 5000,
+ };
+ return false;
+ }
+
+ const segments = {};
+ for (const [layer, prompts] of Object.entries(imagePromptsMulti.val)) {
+ await drawSegment(getClicks(prompts), layer, false);
+ const canvas = document.getElementById("mask-canvas");
+ const base64Image = canvas.toDataURL();
+ segments[layer] = base64Image;
+ // download image
+ // const a = document.createElement("a");
+ // a.href = base64Image;
+ // a.download = layer + ".png";
+ // a.click();
+ }
+ await api.fetchApi("/segments", {
+ method: "POST",
+ body: JSON.stringify({
+ name: embeddingID.val,
+ segments,
+ }),
+ });
+ return true;
+}
+
async function handleClick(e) {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
@@ -59,9 +380,16 @@ async function handleClick(e) {
imageSize.val.imgScale
);
+ let label;
+ if (isMobileDevice()) {
+ label = positivePrompt.val ? 1 : 0;
+ } else {
+ label = e.isRight ? 0 : 1;
+ }
+
imagePrompts.val = [
...imagePrompts.val,
- { x: relativeX, y: relativeY, label: e.isRight ? 0 : 1 },
+ { x: relativeX, y: relativeY, label },
];
await drawSegment(getClicks());
updateImagePrompts();
@@ -87,15 +415,16 @@ function handleImageSize(image) {
return { height: h, width: w, samScale, imgScale };
}
-export function getClicks() {
- return imagePrompts.val.map((point) => ({
+export function getClicks(prompts) {
+ return (prompts || imagePrompts.val).map((point) => ({
x: point.x,
y: point.y,
clickType: point.label,
+ isAuto: point.isAuto,
}));
}
-export async function drawSegment(clicks) {
+export async function drawSegment(clicks, layer, drawBox = true) {
const canvas = document.getElementById("mask-canvas");
const ctx = canvas.getContext("2d");
if (clicks.length === 0) {
@@ -103,16 +432,34 @@ export async function drawSegment(clicks) {
return;
}
if (embeddings.val) {
- const mask = await runONNX(clicks, embeddings.val);
+ const box = enableAutoSegment.val
+ ? boxesMulti.val[layer || selectedLayer.val]
+ : null;
+ const filteredClicks = enableAutoSegment.val
+ ? clicks
+ : clicks.filter((click) => !click.isAuto);
+ if (filteredClicks.length === 0) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ return;
+ }
+ const mask = await runONNX(filteredClicks, embeddings.val, box);
if (mask) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(mask, 0, 0);
+ if (box && drawBox) {
+ ctx.strokeStyle = "green";
+ ctx.lineWidth = 5;
+ ctx.strokeRect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1);
+ }
}
}
}
export function LayerEditor() {
let realTimeSegment = true;
+
+ const showSidebar = van.state(true);
+
document.addEventListener("keydown", (e) => {
if (showImageEditor.val && e.code === "Tab") {
e.preventDefault();
@@ -126,36 +473,97 @@ export function LayerEditor() {
return div(
{
class: () =>
- "absolute flex bg-gray-900 bg-opacity-50 top-0 w-full h-full pointer-events-auto " +
+ "absolute flex bg-gray-900 bg-opacity-50 top-0 w-full h-full pointer-events-auto z-[1000] " +
(showImageEditor.val ? "" : "hidden"),
},
- button(
+ div(
{
- class: () =>
- "btn btn-circle flex flex-row btn-ghost normal-case absolute p-0 rounded-md left-2 top-0 z-[200] w-fit",
- onclick: () => {
- console.log("close");
- showImageEditor.val = false;
- api.fetchApi("/segments_order", {
- method: "POST",
- body: JSON.stringify({
- name: embeddingID.val,
- order: Object.keys(imagePromptsMulti.val),
- }),
- });
- },
+ class:
+ "absolute top-4 left-4 right-0 flex w-full gap-2 justify-start z-[200]",
},
- span({
- class: "iconify text-lg",
- "data-icon": "ic:baseline-arrow-back",
- "data-inline": "false",
- }),
- div("Back")
+ button(
+ {
+ class: () => "btn btn-neutral flex flex-row normal-case rounded-md",
+ onclick: async () => {
+ console.log("close");
+ showImageEditor.val = false;
+ await uploadSegments();
+
+ const isEqual = allImagePrompts.val.map(
+ (x) =>
+ JSON.stringify(imagePromptsMulti.val) ===
+ JSON.stringify(x.prompt)
+ );
+ if (!isEqual.includes(true))
+ allImagePrompts.val = [
+ ...allImagePrompts.val,
+ {
+ version: "v" + allImagePrompts.val.length,
+ prompt: imagePromptsMulti.val,
+ },
+ ];
+
+ // api.fetchApi("/segments_order", {
+ // method: "POST",
+ // body: JSON.stringify({
+ // name: embeddingID.val,
+ // order: Object.keys(imagePromptsMulti.val),
+ // }),
+ // });
+ },
+ },
+ span({
+ class: "iconify text-lg",
+ "data-icon": "ic:baseline-arrow-back",
+ "data-inline": "false",
+ }),
+ div("Back")
+ ),
+ button(
+ {
+ class: () => "btn btn-neutral flex flex-row normal-case rounded-md",
+ onclick: () => (showSidebar.val = !showSidebar.val),
+ },
+ div(() => (showSidebar.val ? "Hide UI" : "Show UI"))
+ ),
+ button(
+ {
+ class: () => "btn btn-neutral flex flex-row normal-case rounded-md",
+ onclick: () => {
+ enableAutoSegment.val = !enableAutoSegment.val;
+ drawSegment(getClicks());
+ },
+ },
+ () => (enableAutoSegment.val ? "Auto Segment On" : "Auto Segment Off")
+ ),
+ button(
+ {
+ class: () => "btn btn-neutral flex flex-row normal-case rounded-md",
+ onclick: () => {
+ enableBackgroundRemover.val = !enableBackgroundRemover.val;
+ setRemoveBackgroundNode();
+ },
+ },
+ () =>
+ enableBackgroundRemover.val
+ ? "Background Remover On"
+ : "Background Remover Off"
+ ),
+ button(
+ {
+ class: () =>
+ `btn btn-neutral flex flex-row normal-case rounded-md ${
+ isMobileDevice() ? "" : "hidden"
+ }`,
+ onclick: () => (positivePrompt.val = !positivePrompt.val),
+ },
+ div(() => (positivePrompt.val ? "Positive" : "Negative"))
+ )
),
div(
{
class:
- "hidden w-full flex justify-center absolute top-0 left-0 right-0 items-center",
+ "hidden w-full justify-center absolute top-0 left-0 right-0 items-center",
},
button(
{
@@ -182,10 +590,11 @@ export function LayerEditor() {
id: "image-container",
},
img({
+ id: "image",
class:
"fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2",
src: imageUrl,
- onload: (e) => {
+ onload: async (e) => {
imageSize.val = handleImageSize(e.target);
document.getElementById("image-container").style.scale =
@@ -240,19 +649,25 @@ export function LayerEditor() {
}
},
}),
- canvas({
- class:
- "pointer-events-none fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-80",
- id: "mask-canvas",
- }),
- () => {
- return div(
+ () =>
+ canvas({
+ class:
+ "pointer-events-none fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-80",
+ style: () =>
+ `width: ${imageContainerSize.val.width}px; height: ${imageContainerSize.val.height}px;`,
+ id: "mask-canvas",
+ }),
+ () =>
+ div(
{
class: "absolute w-full h-full pointer-events-none",
style: () =>
`width: ${imageContainerSize.val.width}px; height: ${imageContainerSize.val.height}px;`,
},
- ...imagePrompts.val?.map((point) => {
+ ...(enableAutoSegment.val
+ ? imagePrompts.val
+ : imagePrompts.val?.filter((click) => !click.isAuto)
+ ).map((point) => {
return button({
style: () =>
`left: ${
@@ -274,9 +689,8 @@ export function LayerEditor() {
},
});
})
- );
- }
+ )
),
- SideBar()
+ () => (showSidebar.val ? SideBar() : div())
);
}
diff --git a/js/Loading.js b/js/Loading.js
index 27b01fa..9ff88e9 100644
--- a/js/Loading.js
+++ b/js/Loading.js
@@ -6,7 +6,7 @@ export function Loading() {
return div(
{
class: () =>
- "absolute flex flex-col justify-center items-center top-0 left-0 bg-gray-900 bg-opacity-50 pointer-events-auto w-full h-full " +
+ "absolute flex flex-col justify-center items-center top-0 left-0 bg-gray-900 bg-opacity-50 pointer-events-auto w-full h-full z-[1001] " +
(showLoading.val ? "" : "hidden"),
},
span({
diff --git a/js/SideBar.js b/js/SideBar.js
index dbd39c1..e287d5e 100644
--- a/js/SideBar.js
+++ b/js/SideBar.js
@@ -5,6 +5,7 @@ import {
imagePromptsMulti,
targetNode,
showImageEditor,
+ allImagePrompts,
} from "./state.js";
import { van } from "./van.js";
const {
@@ -27,7 +28,8 @@ van.derive(() => {
if (
showImageEditor.val &&
targetNode.val != undefined &&
- targetNode.val.outputs && targetNode.val.type === 'SAM MultiLayer'
+ targetNode.val.outputs &&
+ targetNode.val.type === "SAM MultiLayer"
) {
const outputNames = targetNode.val.outputs.map((x) => x.name).slice(1);
const record = Object.keys(imagePromptsMulti.val);
@@ -60,185 +62,232 @@ export function SideBar() {
const layer_to_delete = van.state("");
return div(
- {
- class:
- "ml-2 z-100 w-fit flex-col flex justify-center absolute top-0 left-0 bottom-0 items-start gap-2",
- },
-
- () => {
- const layers = Object.entries(imagePromptsMulti.val);
- return ul(
- {
- class: "menu bg-base-200 w-56 rounded-box text-base-content ",
- },
- layers.length === 0 ? li(a("Empty layer")) : null,
- ...layers.map(([key, value]) => {
- return li(
- a(
- {
- class: () =>
- `normal-case text-start items-start flex items-center justify-between ${
- selectedLayer.val === key ? "active" : ""
- }`,
- onclick: () => {
- selectedLayer.val = key;
- imagePrompts.val = imagePromptsMulti.val[key];
- drawSegment(getClicks());
- },
- },
- key,
- div(
- {},
- button(
- {
- class:
- "btn btn-circle btn-xs btn-ghost group hover:text-red-500",
- onclick: (e) => {
- console.log("clear");
- e.preventDefault();
- e.stopPropagation();
- imagePrompts.val = [];
- imagePromptsMulti.val[key] = [];
- drawSegment([]);
- updateImagePrompts();
- },
- },
- span({
- class: "iconify",
- "data-icon": "ant-design:clear-outlined",
- "data-inline": "false",
- })
- ),
- button(
- {
- class:
- "btn btn-circle btn-xs btn-ghost group hover:text-red-500",
- onclick: (e) => {
- console.log("delete");
- e.preventDefault();
- e.stopPropagation();
- layer_to_delete.val = key;
- setTimeout(() => {
- delete_layer_dialog.showModal();
- }, 0);
- },
- },
- span({
- class: "iconify",
- "data-icon": "ic:baseline-delete",
- "data-inline": "false",
- })
- )
- )
- )
- );
- }),
- div({ class: "divider !py-0 my-0" }),
- li(
- a(
- {
- class: "flex items-center justify-between",
- onclick: () => {
- my_modal_3.showModal();
- },
- },
- "New Layer",
- span({
- class: "iconify",
- "data-icon": "ic:outline-plus",
- "data-inline": "false",
- })
- )
- )
- );
- },
- () =>
- ConfirmDialog(
- {
- id: "delete_layer_dialog",
- title: "Delete Layer: " + layer_to_delete.val,
- onsubmit: () => {
- imagePromptsMulti.val = Object.fromEntries(
- Object.entries(imagePromptsMulti.val).filter(
- ([key, value]) => key !== layer_to_delete.val
- )
- );
- console.log(imagePromptsMulti.val);
- if (selectedLayer.val === layer_to_delete.val) {
- // Select another layer if there is one
- if (Object.keys(imagePromptsMulti.val).length > 0) {
- selectedLayer.val = Object.keys(imagePromptsMulti.val)[0];
- imagePrompts.val = imagePromptsMulti.val[selectedLayer.val];
- } else {
- selectedLayer.val = "";
- imagePrompts.val = [];
- }
- }
- targetNode.val.graph.change();
- updateImagePrompts();
- delete_layer_dialog.close();
+ div(
+ {
+ class:
+ "ml-2 z-100 w-fit flex-col flex justify-center absolute top-0 left-0 bottom-0 items-start gap-2",
+ },
+ () => {
+ const layers = Object.entries(imagePromptsMulti.val);
+ return ul(
+ {
+ class: "menu bg-base-200 w-56 rounded-box text-base-content ",
},
- },
- p("Are you sure you want to delete this layer?")
- ),
- () =>
- dialog(
- { id: "my_modal_3", class: "modal" },
- div(
- { class: "modal-box text-base-content" },
- form(
+ button(
{
- class: "gap-2 flex flex-col",
- method: "dialog",
- onsubmit: (e) => {
- console.log("add new layer");
- e.preventDefault();
- const inputText = e.target.elements[1].value;
- imagePromptsMulti.val = {
- ...imagePromptsMulti.val,
- [inputText]: [],
- };
- console.log(inputText, imagePromptsMulti.val);
- my_modal_3.close();
- e.target.elements[1].value = "";
-
- selectedLayer.val = inputText;
- imagePrompts.val = imagePromptsMulti.val[inputText];
- drawSegment(getClicks());
+ onclick: () => {
+ layers.map(([key, value]) => {
+ imagePrompts.val = [];
+ imagePromptsMulti.val[key] = [];
+ });
+ drawSegment([]);
updateImagePrompts();
},
+ class: "btn btn-ghost normal-case flex",
},
- button(
+ "Clear ALL"
+ ),
+ layers.length === 0 ? li(a("Empty layer")) : null,
+ ...layers.map(([key, value]) => {
+ return li(
+ a(
+ {
+ class: () =>
+ `normal-case text-start items-start flex items-center justify-between ${
+ selectedLayer.val === key ? "active" : ""
+ }`,
+ onclick: () => {
+ selectedLayer.val = key;
+ imagePrompts.val = imagePromptsMulti.val[key];
+ drawSegment(getClicks());
+ },
+ },
+ key,
+ div(
+ {},
+ button(
+ {
+ class:
+ "btn btn-circle btn-xs btn-ghost group hover:text-red-500",
+ onclick: (e) => {
+ console.log("clear");
+ e.preventDefault();
+ e.stopPropagation();
+ imagePrompts.val = [];
+ imagePromptsMulti.val[key] = [];
+ drawSegment([]);
+ updateImagePrompts();
+ },
+ },
+ span({
+ class: "iconify",
+ "data-icon": "ant-design:clear-outlined",
+ "data-inline": "false",
+ })
+ ),
+ button(
+ {
+ class:
+ "btn btn-circle btn-xs btn-ghost group hover:text-red-500",
+ onclick: (e) => {
+ console.log("delete");
+ e.preventDefault();
+ e.stopPropagation();
+ layer_to_delete.val = key;
+ setTimeout(() => {
+ delete_layer_dialog.showModal();
+ }, 0);
+ },
+ },
+ span({
+ class: "iconify",
+ "data-icon": "ic:baseline-delete",
+ "data-inline": "false",
+ })
+ )
+ )
+ )
+ );
+ }),
+ div({ class: "divider !py-0 my-0" }),
+ li(
+ a(
{
- type: "button",
- class: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2",
- onclick: (e) => {
- e.stopPropagation();
- my_modal_3.close();
+ class: "flex items-center justify-between",
+ onclick: () => {
+ my_modal_3.showModal();
},
},
- "✕"
- ),
- h3(
- { class: "font-bold text-lg text-base-content" },
- "Add new layer!"
- ),
- input({
- type: "text",
- placeholder: "Type here",
- class: "input input-bordered w-full",
- autofocus: true,
- }),
- button(
+ "New Layer",
+ span({
+ class: "iconify",
+ "data-icon": "ic:outline-plus",
+ "data-inline": "false",
+ })
+ )
+ )
+ );
+ },
+ () =>
+ ConfirmDialog(
+ {
+ id: "delete_layer_dialog",
+ title: "Delete Layer: " + layer_to_delete.val,
+ onsubmit: () => {
+ imagePromptsMulti.val = Object.fromEntries(
+ Object.entries(imagePromptsMulti.val).filter(
+ ([key, value]) => key !== layer_to_delete.val
+ )
+ );
+ console.log(imagePromptsMulti.val);
+ if (selectedLayer.val === layer_to_delete.val) {
+ // Select another layer if there is one
+ if (Object.keys(imagePromptsMulti.val).length > 0) {
+ selectedLayer.val = Object.keys(imagePromptsMulti.val)[0];
+ imagePrompts.val = imagePromptsMulti.val[selectedLayer.val];
+ } else {
+ selectedLayer.val = "";
+ imagePrompts.val = [];
+ }
+ }
+ targetNode.val.graph.change();
+ updateImagePrompts();
+ delete_layer_dialog.close();
+ },
+ },
+ p("Are you sure you want to delete this layer?")
+ ),
+ () =>
+ dialog(
+ { id: "my_modal_3", class: "modal" },
+ div(
+ { class: "modal-box text-base-content" },
+ form(
{
- type: "submit",
- class: "btn btn-sm btn-ghost place-self-end",
+ class: "gap-2 flex flex-col",
+ method: "dialog",
+ onsubmit: (e) => {
+ console.log("add new layer");
+ e.preventDefault();
+ const inputText = e.target.elements[1].value;
+ imagePromptsMulti.val = {
+ ...imagePromptsMulti.val,
+ [inputText]: [],
+ };
+ console.log(inputText, imagePromptsMulti.val);
+ my_modal_3.close();
+ e.target.elements[1].value = "";
+
+ selectedLayer.val = inputText;
+ imagePrompts.val = imagePromptsMulti.val[inputText];
+ drawSegment(getClicks());
+ updateImagePrompts();
+ },
},
- "Confirm"
+ button(
+ {
+ type: "button",
+ class:
+ "btn btn-sm btn-circle btn-ghost absolute right-2 top-2",
+ onclick: (e) => {
+ e.stopPropagation();
+ my_modal_3.close();
+ },
+ },
+ "✕"
+ ),
+ h3(
+ { class: "font-bold text-lg text-base-content" },
+ "Add new layer!"
+ ),
+ input({
+ type: "text",
+ placeholder: "Type here",
+ class: "input input-bordered w-full",
+ autofocus: true,
+ }),
+ button(
+ {
+ type: "submit",
+ class: "btn btn-sm btn-ghost place-self-end",
+ },
+ "Confirm"
+ )
)
)
)
- )
+ ),
+ div(
+ {
+ class:
+ "ml-2 z-100 w-fit flex-col flex justify-center absolute top-0 right-0 bottom-0 items-start gap-2 bg-transparent",
+ },
+ () => {
+ return ul(
+ {
+ class: "menu bg-base-200 w-56 rounded-box text-base-content ",
+ },
+ span("Segment History"),
+ ...allImagePrompts.val.map((e) =>
+ li(
+ a(
+ {
+ class:
+ "normal-case text-start flex items-center justify-between",
+ onclick: async () => {
+ imagePromptsMulti.val = e.prompt;
+ imagePrompts.val = imagePromptsMulti.val[selectedLayer.val];
+ drawSegment(getClicks());
+ updateImagePrompts();
+ },
+ },
+ e.version
+ )
+ )
+ )
+ );
+ }
+ )
);
}
diff --git a/js/dialog.js b/js/dialog.js
index a609b66..4023da4 100644
--- a/js/dialog.js
+++ b/js/dialog.js
@@ -26,6 +26,7 @@ export class InfoDialog extends ComfyDialog {
this.textElement.replaceChildren(html);
}
this.element.style.display = "flex";
+ this.element.style.zIndex = 1001;
}
}
diff --git a/js/index.js b/js/index.js
index c4c3a30..d188493 100644
--- a/js/index.js
+++ b/js/index.js
@@ -16,6 +16,7 @@ import {
shareLoading,
previewModelId,
embeddingID,
+ enableAutoSegment,
} from "./state.js";
import { van } from "./van.js";
import { app } from "./app.js";
@@ -23,8 +24,17 @@ import { api } from "./api.js";
import { Container } from "./Container.js";
import { initModel, loadNpyTensor } from "./onnx.js";
import "https://code.iconify.design/3/3.1.0/iconify.min.js";
-import { drawSegment, getClicks } from "./LayerEditor.js";
+import {
+ autoSegment,
+ drawSegment,
+ getClicks,
+ segmented,
+} from "./LayerEditor.js";
import { infoDialog } from "./dialog.js";
+import { sharedAvatarLink } from "./AvatarPreview.js";
+import { updateImagePrompts } from "./LayerEditor.js";
+
+export const generatedImages = {};
const stylesheet = document.createElement("link");
stylesheet.setAttribute("type", "text/css");
@@ -73,15 +83,15 @@ LGraphGroup.prototype.repositionNodes = function () {
let sortedNodes = this._nodes.sort((a, b) => a.pos[1] - b.pos[1]);
// Separate input and output nodes from the rest
const inputNodes = sortedNodes.filter(
- (node) => node.properties.routeType === "input",
+ (node) => node.properties.routeType === "input"
);
const outputNodes = sortedNodes.filter(
- (node) => node.properties.routeType === "output",
+ (node) => node.properties.routeType === "output"
);
const otherNodes = sortedNodes.filter(
(node) =>
node.properties.routeType !== "input" &&
- node.properties.routeType !== "output",
+ node.properties.routeType !== "output"
);
// Concatenate the arrays so that input nodes are first and output nodes are last
@@ -215,7 +225,7 @@ function openInAvatechEditor(url, fileName) {
blendshapes: targetNode.val.widgets.find((x) => x.name === "shape_flow")
.value,
},
- "*",
+ "*"
);
}
@@ -236,15 +246,35 @@ function getInputWidgetValue(node, inputIndex, widgetName) {
/** @type {LGraphNode} */
let nodea = graph._nodes_by_id[targetLink.origin_id];
- while (nodea.type == "Reroute") {
+ while (nodea.type === "Reroute") {
nodea = nodea.getInputNode(0);
}
console.log(targetLink, nodea);
- console.log(nodea.getInputNode(0, true));
+ if (nodea.type === "LoadImage") {
+ /** @type {string} */
+ const isGeneratedImage = false;
+ return [
+ isGeneratedImage,
+ nodea.widgets.find((x) => x.name === widgetName).value,
+ ];
+ }
+
+ const saveImageNodeLink = nodea.outputs
+ .find((x) => x.type === "IMAGE")
+ .links.find((link) => {
+ const targetLink = graph.links[link];
+ const targetNode = graph._nodes_by_id[targetLink.target_id];
+ if (targetNode.type === "SaveImage") {
+ return true;
+ }
+ });
+ const saveImageNode =
+ graph._nodes_by_id[graph.links[saveImageNodeLink].target_id];
/** @type {string} */
- return nodea.widgets.find((x) => x.name === widgetName).value;
+ const isGeneratedImage = true;
+ return [isGeneratedImage, generatedImages[saveImageNode.id]];
}
/**
@@ -252,10 +282,14 @@ function getInputWidgetValue(node, inputIndex, widgetName) {
* @param {LGraphNode} node
*/
function showMyImageEditor(node) {
- let connectedImageFileName = getInputWidgetValue(node, 0, "image");
+ let [isGeneratedImage, connectedImageFileName] = getInputWidgetValue(
+ node,
+ 0,
+ "image"
+ );
if (!connectedImageFileName) {
alertDialog.val = {
- text: "Please connect an image first",
+ text: "Please connect or generate an image first",
time: 3000,
};
return;
@@ -281,14 +315,16 @@ function showMyImageEditor(node) {
method: "POST",
body: JSON.stringify({
image: connectedImageFileName,
+ isGeneratedImage,
embedding_id: id,
ckpt,
+ // remote: true,
}),
})
.then(() => {
showLoading.val = false;
const v = JSON.parse(
- node.widgets.find((x) => x.name === "image_prompts_json").value,
+ node.widgets.find((x) => x.name === "image_prompts_json").value
);
if (!Array.isArray(v)) {
@@ -303,19 +339,23 @@ function showMyImageEditor(node) {
imagePrompts.val = v;
}
showImageEditor.val = true;
+ const subfolder =
+ isGeneratedImage || split.length === 1 ? "" : split[0];
imageUrl.val = api.apiURL(
- `/view?filename=${encodeURIComponent(
- connectedImageFileName,
- )}&type=input&subfolder=${split.length > 1 ? split[0] : ""}`,
+ `/view?filename=${encodeURIComponent(connectedImageFileName)}&type=${
+ isGeneratedImage ? "output" : "input"
+ }&subfolder=${subfolder}`
);
const embeedingUrl = api.apiURL(
`/view?filename=${encodeURIComponent(
- `${id}_${modelType}.npy`,
- )}&type=output&subfolder=`,
+ `${id}_${modelType}.npy`
+ )}&type=output&subfolder=`
);
- loadNpyTensor(embeedingUrl).then((tensor) => {
+ loadNpyTensor(embeedingUrl).then(async (tensor) => {
embeddings.val = tensor;
+ if (enableAutoSegment.val && !segmented.val) await autoSegment();
drawSegment(getClicks());
+ updateImagePrompts();
});
targetNode.val = node;
})
@@ -332,6 +372,15 @@ const ext = {
getCustomWidgets(app) {
return {
SAM_PROMPTS(node, inputName, inputData, app) {
+ const asd = document.createElement("div");
+ Object.assign(asd, {
+ id: "sam",
+ onclick: () => {
+ showMyImageEditor(node);
+ },
+ });
+ document.body.append(asd);
+
const btn = node.addWidget("button", "Edit prompt", "", () => {
showMyImageEditor(node);
btn.serialize = false;
@@ -345,7 +394,7 @@ const ext = {
targetNode.val = node;
openInAvatechEditor(
"https://editor.avatech.ai?comfyui=true",
- fileName.val,
+ fileName.val
);
// openInAvatechEditor("http://localhost:3006?comfyui=true", fileName.val);
});
@@ -378,6 +427,24 @@ const ext = {
widget: btn,
};
},
+ GROUP_OPS(node, inputName, inputData, app) {
+ const btn = node.addWidget("button", "Add OBJ", "", () => {
+ node.addInput("BPY_OBJ" + (node.inputs.length + 1), "BPY_OBJ");
+ node.graph.change();
+ });
+ return {
+ widget: btn,
+ };
+ },
+ GROUP_OPS_DELETE(node, inputName, inputData, app) {
+ const btn = node.addWidget("button", "Delete OBJ", "", () => {
+ node.removeInput(node.inputs.length - 1);
+ node.graph.change();
+ });
+ return {
+ widget: btn,
+ };
+ },
};
},
@@ -407,9 +474,13 @@ const ext = {
});
api.addEventListener("executed", (evt) => {
+ const images = evt.detail?.output.images;
+ if (images?.length > 0 && images[0].type === "output") {
+ generatedImages[evt.detail.node] = images[0].filename;
+ }
if (evt.detail?.output.gltfFilename) {
const viewer = document.getElementById(
- "avatech-viewer-iframe",
+ "avatech-viewer-iframe"
).contentWindow;
const gltfFilename =
@@ -437,14 +508,14 @@ const ext = {
avatarURL: gltfFilename,
blendshapes: evt.detail?.output.SHAPE_FLOW[0],
}),
- "*",
+ "*"
);
}
});
window.addEventListener(
"keydown",
- (event) => {
+ async (event) => {
if (event.key === "Escape") {
event.preventDefault();
if (my_modal_3.open) {
@@ -452,19 +523,20 @@ const ext = {
} else {
showImageEditor.val = false;
showEditor.val = false;
- api.fetchApi("/segments_order", {
- method: "POST",
- body: JSON.stringify({
- name: embeddingID.val,
- order: Object.keys(imagePromptsMulti.val),
- }),
- });
+ await uploadSegments();
+ // api.fetchApi("/segments_order", {
+ // method: "POST",
+ // body: JSON.stringify({
+ // name: embeddingID.val,
+ // order: Object.keys(imagePromptsMulti.val),
+ // }),
+ // });
}
}
},
{
capture: true,
- },
+ }
);
graphCanvas.addEventListener("keydown", (event) => {
@@ -540,7 +612,7 @@ const ext = {
a.href = url;
a.setAttribute(
"download",
- new URLSearchParams(url.search).get("filename"),
+ new URLSearchParams(url.search).get("filename")
);
document.body.append(a);
a.click();
@@ -570,7 +642,7 @@ const ext = {
a.href = url;
a.setAttribute(
"download",
- new URLSearchParams(url.search).get("filename"),
+ new URLSearchParams(url.search).get("filename")
);
document.body.append(a);
a.click();
@@ -583,7 +655,7 @@ const ext = {
callback: () => {
openInAvatechEditor(
"http://localhost:3006?comfyui=true",
- gltfFilename,
+ gltfFilename
);
},
});
@@ -593,7 +665,7 @@ const ext = {
callback: () => {
openInAvatechEditor(
"https://editor.avatech.ai?comfyui=true",
- gltfFilename,
+ gltfFilename
);
},
});
@@ -619,13 +691,16 @@ const ext = {
nodeData.input.required.obj = ["MESH_GROUP_CONFIG"];
nodeData.input.required.del_obj = ["MESH_GROUP_DELETE"];
break;
+ case "GroupOps":
+ nodeData.input.required.obj = ["GROUP_OPS"];
+ nodeData.input.required.del_obj = ["GROUP_OPS_DELETE"];
default:
break;
}
},
};
-async function uploadPreview() {
+export async function uploadPreview() {
if (fileName.val == "")
app.ui.dialog.show("Please create your avatar first.");
else {
@@ -646,12 +721,9 @@ async function uploadPreview() {
body: file,
}).catch((error) => console.error(error));
- infoDialog.show(
- `Preview avatar url: https://editor.avatech.ai/viewer?objectId=` +
- labData.modelId +
- ``,
- );
+ sharedAvatarLink.val = `https://editor.avatech.ai/viewer?avatarId=${labData?.modelId}`;
previewModelId.val = labData.modelId;
+ return labData;
}
}
@@ -664,6 +736,33 @@ function injectUIComponentToComfyuimenu() {
localStorage.setItem("showPreview", showPreview.val);
};
+ const apiFormat = document.createElement("button");
+ const a = document.createElement("a");
+ apiFormat.textContent = "Save API Format (Avatech)";
+ apiFormat.onclick = () => {
+ let filename = "workflow_api.json";
+ filename = prompt("Save workflow (API) as:", filename);
+ if (!filename) return;
+ if (!filename.toLowerCase().endsWith(".json")) {
+ filename += ".json";
+ }
+ app.graphToPrompt().then(p=>{
+ console.log('fkfk');
+ let json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
+ json = json.replace(/"seed": (\d+)/g, `"seed": "SEED"`).replace(/"image": "(?!.*mask.*\.png).*"/g, '"image": "reference_image_avatech"').replace(/"embedding_id": ".*"/g, '"embedding_id": "embedding_id_avatech"');
+ const blob = new Blob([json], {type: "application/json"});
+ const url = URL.createObjectURL(blob);
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(function () {
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ }, 0);
+ });
+ };
+
const dropdown = document.createElement("div");
dropdown.textContent = "â–¼";
dropdown.className = "dropdownbtn";
@@ -699,9 +798,12 @@ function injectUIComponentToComfyuimenu() {
.then((e) => e.arrayBuffer())
.then((e) => new Uint8Array(e));
- const labData = await fetch("https://labs.avatech.ai/api/share?id=" + previewModelId.val, {
- method: "GET",
- }).then((e) => e.json());
+ const labData = await fetch(
+ "https://labs.avatech.ai/api/share?id=" + previewModelId.val,
+ {
+ method: "GET",
+ }
+ ).then((e) => e.json());
await fetch(labData.url, {
method: "PUT",
@@ -713,14 +815,17 @@ function injectUIComponentToComfyuimenu() {
body: file,
}).catch((error) => console.error(error));
- await fetch("https://labs.avatech.ai/api/purgecdn?id=" + previewModelId.val, {
- method: "GET",
- }).catch((error) => console.error(error));
+ await fetch(
+ "https://labs.avatech.ai/api/purgecdn?id=" + previewModelId.val,
+ {
+ method: "GET",
+ }
+ ).catch((error) => console.error(error));
infoDialog.show(
- `Preview updated: https://editor.avatech.ai/viewer?objectId=` +
- labData.modelId +
- "\n Remember to hard refresh before checking out the new preview!",
+ `Preview updated: https://editor.avatech.ai/viewer?avatarId=` +
+ labData.modelId +
+ "\n Remember to hard refresh before checking out the new preview!"
);
shareLoading.val = false;
@@ -735,7 +840,7 @@ function injectUIComponentToComfyuimenu() {
event: e,
scale: 1.3,
},
- window,
+ window
);
menu.root.classList.add("popup");
};
@@ -758,15 +863,15 @@ function injectUIComponentToComfyuimenu() {
shareAvatar.append(dropdown);
} else {
infoDialog.show(
- `Preview avatar url: https://editor.avatech.ai/viewer?objectId=` +
- previewModelId.val +
- ``,
+ `Preview avatar url: https://editor.avatech.ai/viewer?avatarId=${previewModelId.val}` +
+ `\nChat url: https://labs.avatech.ai?avatarId=${previewModelId.val}`
);
}
};
menu.append(avatarPreview);
menu.append(shareAvatar);
+ menu.append(apiFormat);
shareAvatar.append(dropdown);
}
diff --git a/js/onnx.js b/js/onnx.js
index ec5df53..b0ecf57 100644
--- a/js/onnx.js
+++ b/js/onnx.js
@@ -10,9 +10,11 @@ export let model = null;
// Initialize the ONNX model
export const initModel = async (modelType) => {
try {
- model = await ort.InferenceSession.create(
- `http://127.0.0.1:8188/sam_model?type=${modelType}`
- );
+ if (!model) {
+ model = await ort.InferenceSession.create(
+ `${location.protocol}//${location.host}/sam_model?type=${modelType}`
+ );
+ }
} catch (e) {
console.log(e);
}
@@ -25,7 +27,7 @@ export const loadNpyTensor = async (tensorFile, dType = "float32") => {
return tensor;
};
-export const runONNX = async (clicks, tensor) => {
+export const runONNX = async (clicks, tensor, box) => {
// console.log('tensor', tensor);
try {
if (
@@ -42,6 +44,7 @@ export const runONNX = async (clicks, tensor) => {
clicks,
tensor,
modelScale: imageSize.val,
+ box,
});
if (feeds === undefined) return;
// Run the SAM ONNX model with the feeds returned from modelData()
diff --git a/js/onnx_helper.js b/js/onnx_helper.js
index 3ab436a..a38aade 100644
--- a/js/onnx_helper.js
+++ b/js/onnx_helper.js
@@ -4,7 +4,7 @@
// This source code is licensed under the license found in the
// LICENSE file in the root directory of this source tree.
-const modelData = ({ clicks, tensor, modelScale }) => {
+const modelData = ({ clicks, tensor, modelScale, box }) => {
const imageEmbedding = tensor;
let pointCoords;
let pointLabels;
@@ -18,8 +18,9 @@ const modelData = ({ clicks, tensor, modelScale }) => {
// If there is no box input, a single padding point with
// label -1 and coordinates (0.0, 0.0) should be concatenated
// so initialize the array to support (n + 1) points.
- pointCoords = new Float32Array(2 * (n + 1));
- pointLabels = new Float32Array(n + 1);
+ const numPoints = box ? n + 3 : n + 1;
+ pointCoords = new Float32Array(2 * numPoints);
+ pointLabels = new Float32Array(numPoints);
// Add clicks and scale to what SAM expects
for (let i = 0; i < n; i++) {
@@ -28,15 +29,25 @@ const modelData = ({ clicks, tensor, modelScale }) => {
pointLabels[i] = clicks[i].clickType;
}
- // Add in the extra point/label when only clicks and no box
- // The extra point is at (0, 0) with label -1
- pointCoords[2 * n] = 0.0;
- pointCoords[2 * n + 1] = 0.0;
- pointLabels[n] = -1.0;
+ if (box) {
+ pointCoords[2 * n] = box.x1 * modelScale.samScale;
+ pointCoords[2 * n + 1] = box.y1 * modelScale.samScale;
+ pointLabels[n] = 2;
+
+ pointCoords[2 * n + 2] = box.x2 * modelScale.samScale;
+ pointCoords[2 * n + 3] = box.y2 * modelScale.samScale;
+ pointLabels[n + 1] = 3;
+ } else {
+ // Add in the extra point/label when only clicks and no box
+ // The extra point is at (0, 0) with label -1
+ pointCoords[2 * n] = 0.0;
+ pointCoords[2 * n + 1] = 0.0;
+ pointLabels[n] = -1.0;
+ }
// Create the tensor
- pointCoordsTensor = new ort.Tensor("float32", pointCoords, [1, n + 1, 2]);
- pointLabelsTensor = new ort.Tensor("float32", pointLabels, [1, n + 1]);
+ pointCoordsTensor = new ort.Tensor("float32", pointCoords, [1, numPoints, 2]);
+ pointLabelsTensor = new ort.Tensor("float32", pointLabels, [1, numPoints]);
}
const imageSizeTensor = new ort.Tensor("float32", [
modelScale.height,
diff --git a/js/state.js b/js/state.js
index 6abc818..131edb9 100644
--- a/js/state.js
+++ b/js/state.js
@@ -9,18 +9,30 @@
* @typedef {Object} Point
* @property {number} x - The x coordinate
* @property {number} y - The y coordinate
- * @property {number} label - The label
+ * @property {>} label - The label
+ *
+ * @typedef {Object} Box
+ * @property {number} x1
+ * @property {number} y1
+ * @property {number} x2
+ * @property {number} y2
*/
import { van } from "./van.js";
export const iframeSrc = van.state("https://editor.avatech.ai?comfyui=true");
export const showEditor = van.state(false);
-export const showPreview = van.state(localStorage.getItem("showPreview") == 'true');
-export const previewUrl = van.state("https://editor.avatech.ai/viewer?avatarId=default&debug=true&width=300&height=300&hideTrigger=true");
+// localStorage.getItem("showPreview") == 'true'
+export const showPreview = van.state(true);
+export const previewUrl = van.state(
+ "https://editor.avatech.ai/viewer?avatarId=default&debug=false&width=400&height=400&hideTrigger=true&voiceSelection=true&hideUI=true"
+);
+export const previewImg = van.state("");
+export const previewImgLoading = van.state(false);
+export const enableAutoSegment = van.state(false);
// export const previewUrl = van.state("http://localhost:3006/viewer?avatarId=default&hideUI=true&debug=true&width=300&height=300&showAudioControl=true");
export const isDirty = van.state(false);
-export const fileName = van.state('');
+export const fileName = van.state("");
export const showImageEditor = van.state(false);
export const showLoading = van.state(false);
export const alertDialog = van.state({
@@ -28,7 +40,9 @@ export const alertDialog = van.state({
time: 0,
});
export const shareLoading = van.state(false);
-export const previewModelId = van.state('');
+export const previewModelId = van.state("");
+
+export const isGenerateFlow = van.state(false);
export const loadingCaption = van.state("");
export const imageUrl = van.state("");
@@ -38,9 +52,18 @@ export const imageContainerSize = van.state({
height: 0,
});
+/** @type {State} */
+export const boxes = van.state();
+
+/** @type {State>} */
+export const boxesMulti = van.state({});
+
/** @type {State} */
export const imagePrompts = van.state([]);
+export const allImagePrompts = van.state([{}]);
+
+
/** @type {State>} */
export const imagePromptsMulti = van.state({});
diff --git a/js/styles.css b/js/styles.css
index b07e30f..e69e65c 100644
--- a/js/styles.css
+++ b/js/styles.css
@@ -1,3 +1,4 @@
+@import url('https://fonts.googleapis.com/css2?family=Gabarito&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -134,3 +135,12 @@
.comfy-list-actions button {
font-size: 12px;
}
+
+img {
+ display: none;
+}
+
+
+img[src] {
+ display: block;
+}
diff --git a/js/tw-styles.css b/js/tw-styles.css
index bae1f9c..63baa6d 100644
--- a/js/tw-styles.css
+++ b/js/tw-styles.css
@@ -1,3 +1,5 @@
+@import url('https://fonts.googleapis.com/css2?family=Gabarito&display=swap');
+
/*
! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
*/
@@ -435,54 +437,101 @@ video {
}
:root,
-[data-theme] {
- background-color: hsl(var(--b1) / var(--tw-bg-opacity, 1));
- color: hsl(var(--bc) / var(--tw-text-opacity, 1));
+[data-theme]{
+ background-color: var(--fallback-b1,oklch(var(--b1)/1));
+ color: var(--fallback-bc,oklch(var(--bc)/1));
}
-html {
+@supports not (color: oklch(0 0 0)){
+ :root{
+ color-scheme: light;
+ --fallback-p: #491eff;
+ --fallback-pc: #d4dbff;
+ --fallback-s: #ff41c7;
+ --fallback-sc: #fff9fc;
+ --fallback-a: #00cfbd;
+ --fallback-ac: #00100d;
+ --fallback-n: #2b3440;
+ --fallback-nc: #d7dde4;
+ --fallback-b1: #ffffff;
+ --fallback-b2: #e5e6e6;
+ --fallback-b3: #e5e6e6;
+ --fallback-bc: #1f2937;
+ --fallback-in: #00b3f0;
+ --fallback-inc: #000000;
+ --fallback-su: #00ca92;
+ --fallback-suc: #000000;
+ --fallback-wa: #ffc22d;
+ --fallback-wac: #000000;
+ --fallback-er: #ff6f70;
+ --fallback-erc: #000000;
+ }
+
+ @media (prefers-color-scheme: dark){
+ :root{
+ color-scheme: dark;
+ --fallback-p: #7582ff;
+ --fallback-pc: #050617;
+ --fallback-s: #ff71cf;
+ --fallback-sc: #190211;
+ --fallback-a: #00c7b5;
+ --fallback-ac: #000e0c;
+ --fallback-n: #2a323c;
+ --fallback-nc: #a6adbb;
+ --fallback-b1: #1d232a;
+ --fallback-b2: #191e24;
+ --fallback-b3: #15191e;
+ --fallback-bc: #a6adbb;
+ --fallback-in: #00b3f0;
+ --fallback-inc: #000000;
+ --fallback-su: #00ca92;
+ --fallback-suc: #000000;
+ --fallback-wa: #ffc22d;
+ --fallback-wac: #000000;
+ --fallback-er: #ff6f70;
+ --fallback-erc: #000000;
+ }
+ }
+}
+
+html{
-webkit-tap-highlight-color: transparent;
}
-:root {
+:root{
color-scheme: light;
- --pf: 259 94% 44%;
- --sf: 314 100% 40%;
- --af: 174 75% 39%;
- --nf: 214 20% 14%;
- --in: 198 93% 60%;
- --su: 158 64% 52%;
- --wa: 43 96% 56%;
- --er: 0 91% 71%;
- --inc: 198 100% 12%;
- --suc: 158 100% 10%;
- --wac: 43 100% 11%;
- --erc: 0 100% 14%;
+ --in: 0.7206 0.191 231.6;
+ --su: 0.7441 0.213 164.75;
+ --wa: 0.8471 0.199 83.87;
+ --er: 0.7176 0.221 22.18;
+ --pc: 0.89824 0.06192 275.75;
+ --ac: 0.15352 0.0368 183.61;
+ --inc: 0 0 0;
+ --suc: 0 0 0;
+ --wac: 0 0 0;
+ --erc: 0 0 0;
--rounded-box: 1rem;
--rounded-btn: 0.5rem;
--rounded-badge: 1.9rem;
--animation-btn: 0.25s;
--animation-input: .2s;
- --btn-text-case: uppercase;
--btn-focus-scale: 0.95;
--border-btn: 1px;
--tab-border: 1px;
--tab-radius: 0.5rem;
- --p: 259 94% 51%;
- --pc: 259 96% 91%;
- --s: 314 100% 47%;
- --sc: 314 100% 91%;
- --a: 174 75% 46%;
- --ac: 174 75% 11%;
- --n: 214 20% 21%;
- --nc: 212 19% 87%;
- --b1: 0 0% 100%;
- --b2: 0 0% 95%;
- --b3: 180 2% 90%;
- --bc: 215 28% 17%;
+ --p: 0.4912 0.3096 275.75;
+ --s: 0.6971 0.329 342.55;
+ --sc: 0.9871 0.0106 342.55;
+ --a: 0.7676 0.184 183.61;
+ --n: 0.321785 0.02476 255.701624;
+ --nc: 0.894994 0.011585 252.096176;
+ --b1: 1 0 0;
+ --b2: 0.961151 0 0;
+ --b3: 0.924169 0.00108 197.137559;
+ --bc: 0.278078 0.029596 256.847952;
}
-*, ::before, ::after {
+*, ::before, ::after{
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
@@ -532,7 +581,7 @@ html {
--tw-backdrop-sepia: ;
}
-::backdrop {
+::backdrop{
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
@@ -582,48 +631,107 @@ html {
--tw-backdrop-sepia: ;
}
-.avatar {
+.alert{
+ display: grid;
+ width: 100%;
+ grid-auto-flow: row;
+ align-content: flex-start;
+ align-items: center;
+ justify-items: center;
+ gap: 1rem;
+ text-align: center;
+ border-radius: var(--rounded-box, 1rem);
+ border-width: 1px;
+ --tw-border-opacity: 1;
+ border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
+ padding: 1rem;
+ --tw-text-opacity: 1;
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
+ --alert-bg: var(--fallback-b2,oklch(var(--b2)/1));
+ --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));
+ background-color: var(--alert-bg);
+}
+
+@media (min-width: 640px){
+ .alert{
+ grid-auto-flow: column;
+ grid-template-columns: auto minmax(auto,1fr);
+ justify-items: start;
+ text-align: start;
+ }
+}
+
+.avatar{
position: relative;
display: inline-flex;
}
-.avatar > div {
+.avatar > div{
display: block;
aspect-ratio: 1 / 1;
overflow: hidden;
}
-.avatar img {
+.avatar img{
height: 100%;
width: 100%;
-o-object-fit: cover;
object-fit: cover;
}
-.avatar.placeholder > div {
+.avatar.placeholder > div{
display: flex;
align-items: center;
justify-content: center;
}
-@media (hover:hover) {
- .label a:hover {
+.badge{
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+ transition-duration: 200ms;
+ height: 1.25rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ width: -moz-fit-content;
+ width: fit-content;
+ padding-left: 0.563rem;
+ padding-right: 0.563rem;
+ border-radius: var(--rounded-badge, 1.9rem);
+ border-width: 1px;
+ --tw-border-opacity: 1;
+ border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
+ --tw-text-opacity: 1;
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
+}
+
+@media (hover:hover){
+ .label a:hover{
--tw-text-opacity: 1;
- color: hsl(var(--bc) / var(--tw-text-opacity));
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
}
.menu li > *:not(ul):not(.menu-title):not(details):active,
.menu li > *:not(ul):not(.menu-title):not(details).active,
-.menu li > details > summary:active {
+.menu li > details > summary:active{
--tw-bg-opacity: 1;
- background-color: hsl(var(--n) / var(--tw-bg-opacity));
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
- color: hsl(var(--nc) / var(--tw-text-opacity));
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
}
}
-.btn {
+.btn{
display: inline-flex;
+ height: 3rem;
+ min-height: 3rem;
flex-shrink: 0;
cursor: pointer;
-webkit-user-select: none;
@@ -632,61 +740,46 @@ html {
flex-wrap: wrap;
align-items: center;
justify-content: center;
- border-color: transparent;
- border-color: hsl(var(--b2) / var(--tw-border-opacity));
- text-align: center;
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
- transition-duration: 200ms;
border-radius: var(--rounded-btn, 0.5rem);
- height: 3rem;
+ border-color: transparent;
+ border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1));
padding-left: 1rem;
padding-right: 1rem;
- min-height: 3rem;
+ text-align: center;
font-size: 0.875rem;
line-height: 1em;
gap: 0.5rem;
font-weight: 600;
text-decoration-line: none;
+ transition-duration: 200ms;
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
border-width: var(--border-btn, 1px);
animation: button-pop var(--animation-btn, 0.25s) ease-out;
- text-transform: var(--btn-text-case, uppercase);
- --tw-border-opacity: 1;
- --tw-bg-opacity: 1;
- background-color: hsl(var(--b2) / var(--tw-bg-opacity));
+ transition-property: color, background-color, border-color, opacity, box-shadow, transform;
--tw-text-opacity: 1;
- color: hsl(var(--bc) / var(--tw-text-opacity));
- outline-color: hsl(var(--bc) / 1);
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
+ --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+ outline-color: var(--fallback-bc,oklch(var(--bc)/1));
+ background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1));
}
.btn-disabled,
.btn[disabled],
- .btn:disabled {
+ .btn:disabled{
pointer-events: none;
}
-.btn-circle {
+.btn-circle{
height: 3rem;
width: 3rem;
border-radius: 9999px;
padding: 0px;
}
-.btn-group > input[type="radio"].btn {
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-.btn-group > input[type="radio"].btn:before {
- content: attr(data-title);
-}
-
-.btn:is(input[type="checkbox"]),
-.btn:is(input[type="radio"]) {
+:where(.btn:is(input[type="checkbox"])),
+:where(.btn:is(input[type="radio"])){
width: auto;
-webkit-appearance: none;
-moz-appearance: none;
@@ -694,16 +787,16 @@ html {
}
.btn:is(input[type="checkbox"]):after,
-.btn:is(input[type="radio"]):after {
+.btn:is(input[type="radio"]):after{
--tw-content: attr(aria-label);
content: var(--tw-content);
}
-.collapse:not(td):not(tr):not(colgroup) {
+.collapse:not(td):not(tr):not(colgroup){
visibility: visible;
}
-.collapse {
+.collapse{
position: relative;
display: grid;
overflow: hidden;
@@ -716,13 +809,13 @@ html {
.collapse-title,
.collapse > input[type="checkbox"],
.collapse > input[type="radio"],
-.collapse-content {
+.collapse-content{
grid-column-start: 1;
grid-row-start: 1;
}
.collapse > input[type="checkbox"],
-.collapse > input[type="radio"] {
+.collapse > input[type="radio"]{
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
@@ -731,12 +824,12 @@ html {
.collapse[open],
.collapse-open,
-.collapse:focus:not(.collapse-close) {
+.collapse:focus:not(.collapse-close){
grid-template-rows: auto 1fr;
}
.collapse:not(.collapse-close):has(> input[type="checkbox"]:checked),
-.collapse:not(.collapse-close):has(> input[type="radio"]:checked) {
+.collapse:not(.collapse-close):has(> input[type="radio"]:checked){
grid-template-rows: auto 1fr;
}
@@ -744,13 +837,22 @@ html {
.collapse-open > .collapse-content,
.collapse:focus:not(.collapse-close) > .collapse-content,
.collapse:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-content,
-.collapse:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-content {
+.collapse:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-content{
visibility: visible;
min-height: -moz-fit-content;
min-height: fit-content;
}
-.divider {
+.diff{
+ position: relative;
+ display: grid;
+ width: 100%;
+ overflow: hidden;
+ container-type: inline-size;
+ grid-template-columns: auto 1fr;
+}
+
+.divider{
display: flex;
flex-direction: row;
align-items: center;
@@ -762,28 +864,31 @@ html {
}
.divider:before,
- .divider:after {
- content: "";
- flex-grow: 1;
+ .divider:after{
height: 0.125rem;
width: 100%;
+ flex-grow: 1;
+ --tw-content: '';
+ content: var(--tw-content);
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
}
-.dropdown {
+.dropdown{
position: relative;
display: inline-block;
}
-.dropdown > *:focus {
+.dropdown > *:not(summary):focus{
outline: 2px solid transparent;
outline-offset: 2px;
}
-.dropdown .dropdown-content {
+.dropdown .dropdown-content{
position: absolute;
}
-.dropdown:is(:not(details)) .dropdown-content {
+.dropdown:is(:not(details)) .dropdown-content{
visibility: hidden;
opacity: 0;
transform-origin: top;
@@ -798,75 +903,139 @@ html {
transition-duration: 200ms;
}
+.dropdown-end .dropdown-content{
+ inset-inline-end: 0px;
+}
+
+.dropdown-left .dropdown-content{
+ bottom: auto;
+ inset-inline-end: 100%;
+ top: 0px;
+ transform-origin: right;
+}
+
+.dropdown-right .dropdown-content{
+ bottom: auto;
+ inset-inline-start: 100%;
+ top: 0px;
+ transform-origin: left;
+}
+
+.dropdown-bottom .dropdown-content{
+ bottom: auto;
+ top: 100%;
+ transform-origin: top;
+}
+
+.dropdown-top .dropdown-content{
+ bottom: 100%;
+ top: auto;
+ transform-origin: bottom;
+}
+
+.dropdown-end.dropdown-right .dropdown-content{
+ bottom: 0px;
+ top: auto;
+}
+
+.dropdown-end.dropdown-left .dropdown-content{
+ bottom: 0px;
+ top: auto;
+}
+
.dropdown.dropdown-open .dropdown-content,
.dropdown:not(.dropdown-hover):focus .dropdown-content,
-.dropdown:focus-within .dropdown-content {
+.dropdown:focus-within .dropdown-content{
visibility: visible;
opacity: 1;
}
-@media (hover: hover) {
- .dropdown.dropdown-hover:hover .dropdown-content {
+@media (hover: hover){
+ .dropdown.dropdown-hover:hover .dropdown-content{
visibility: visible;
opacity: 1;
}
- .btn:hover {
+ .btn:hover{
--tw-border-opacity: 1;
- border-color: hsl(var(--b3) / var(--tw-border-opacity));
+ border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));
+ border-color: color-mix(
+ in oklab,
+ oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%,
+ black
+ );
--tw-bg-opacity: 1;
- background-color: hsl(var(--b3) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
+ background-color: color-mix(
+ in oklab,
+ oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%,
+ black
+ );
}
- .btn.glass:hover {
+ @supports not (color: oklch(0 0 0)){
+ .btn:hover{
+ background-color: var(--btn-color, var(--fallback-b2));
+ border-color: var(--btn-color, var(--fallback-b2));
+ }
+ }
+
+ .btn.glass:hover{
--glass-opacity: 25%;
--glass-border-opacity: 15%;
}
- .btn-ghost:hover {
- --tw-border-opacity: 0;
- background-color: hsl(var(--bc) / var(--tw-bg-opacity));
- --tw-bg-opacity: 0.2;
+ .btn-ghost:hover{
+ border-color: transparent;
+ }
+
+ @supports (color: oklch(0 0 0)){
+ .btn-ghost:hover{
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
+ }
}
.btn-disabled:hover,
.btn[disabled]:hover,
- .btn:disabled:hover {
+ .btn:disabled:hover{
--tw-border-opacity: 0;
- background-color: hsl(var(--n) / var(--tw-bg-opacity));
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-bg-opacity: 0.2;
- color: hsl(var(--bc) / var(--tw-text-opacity));
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
--tw-text-opacity: 0.2;
}
- .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover {
- --tw-border-opacity: 1;
- border-color: hsl(var(--pf) / var(--tw-border-opacity));
- --tw-bg-opacity: 1;
- background-color: hsl(var(--pf) / var(--tw-bg-opacity));
+ .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover{
+ background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
+ border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
}
- .dropdown.dropdown-hover:hover .dropdown-content {
+ .dropdown.dropdown-hover:hover .dropdown-content{
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
- :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover {
+ :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover{
cursor: pointer;
- background-color: hsl(var(--bc) / 0.1);
--tw-text-opacity: 1;
- color: hsl(var(--bc) / var(--tw-text-opacity));
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
outline: 2px solid transparent;
outline-offset: 2px;
}
+
+ @supports (color: oklch(0 0 0)){
+ :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover{
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
+ }
+ }
}
-.dropdown:is(details) summary::-webkit-details-marker {
+.dropdown:is(details) summary::-webkit-details-marker{
display: none;
}
-.label {
+.label{
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
@@ -879,7 +1048,7 @@ html {
padding-bottom: 0.5rem;
}
-.input {
+.input{
flex-shrink: 1;
height: 3rem;
padding-left: 1rem;
@@ -887,37 +1056,82 @@ html {
font-size: 1rem;
line-height: 2;
line-height: 1.5rem;
+ border-radius: var(--rounded-btn, 0.5rem);
border-width: 1px;
- border-color: hsl(var(--bc) / var(--tw-border-opacity));
- --tw-border-opacity: 0;
+ border-color: transparent;
--tw-bg-opacity: 1;
- background-color: hsl(var(--b1) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
+}
+
+.join{
+ display: inline-flex;
+ align-items: stretch;
border-radius: var(--rounded-btn, 0.5rem);
}
-.input-group > .input {
- isolation: isolate;
+.join :where(.join-item){
+ border-start-end-radius: 0;
+ border-end-end-radius: 0;
+ border-end-start-radius: 0;
+ border-start-start-radius: 0;
}
-.input-group > *,
- .input-group > .input,
- .input-group > .textarea,
- .input-group > .select {
- border-radius: 0px;
+.join .join-item:not(:first-child):not(:last-child),
+ .join *:not(:first-child):not(:last-child) .join-item{
+ border-start-end-radius: 0;
+ border-end-end-radius: 0;
+ border-end-start-radius: 0;
+ border-start-start-radius: 0;
+}
+
+.join .join-item:first-child:not(:last-child),
+ .join *:first-child:not(:last-child) .join-item{
+ border-start-end-radius: 0;
+ border-end-end-radius: 0;
}
.join .dropdown .join-item:first-child:not(:last-child),
- .join *:first-child:not(:last-child) .dropdown .join-item {
- border-top-right-radius: inherit;
- border-bottom-right-radius: inherit;
+ .join *:first-child:not(:last-child) .dropdown .join-item{
+ border-start-end-radius: inherit;
+ border-end-end-radius: inherit;
}
-.link {
+.join :where(.join-item:first-child:not(:last-child)),
+ .join :where(*:first-child:not(:last-child) .join-item){
+ border-end-start-radius: inherit;
+ border-start-start-radius: inherit;
+}
+
+.join .join-item:last-child:not(:first-child),
+ .join *:last-child:not(:first-child) .join-item{
+ border-end-start-radius: 0;
+ border-start-start-radius: 0;
+}
+
+.join :where(.join-item:last-child:not(:first-child)),
+ .join :where(*:last-child:not(:first-child) .join-item){
+ border-start-end-radius: inherit;
+ border-end-end-radius: inherit;
+}
+
+@supports not selector(:has(*)){
+ :where(.join *){
+ border-radius: inherit;
+ }
+}
+
+@supports selector(:has(*)){
+ :where(.join *:has(.join-item)){
+ border-radius: inherit;
+ }
+}
+
+.link{
cursor: pointer;
text-decoration-line: underline;
}
-.mask {
+.mask{
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
@@ -926,7 +1140,7 @@ html {
mask-position: center;
}
-.menu {
+.menu{
display: flex;
flex-direction: column;
flex-wrap: wrap;
@@ -935,15 +1149,17 @@ html {
padding: 0.5rem;
}
-.menu :where(li ul) {
+.menu :where(li ul){
position: relative;
white-space: nowrap;
- margin-left: 1rem;
- padding-left: 0.5rem;
+ -webkit-margin-start: 1rem;
+ margin-inline-start: 1rem;
+ -webkit-padding-start: 0.5rem;
+ padding-inline-start: 0.5rem;
}
.menu :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)),
- .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
+ .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)){
display: grid;
grid-auto-flow: column;
align-content: flex-start;
@@ -955,19 +1171,19 @@ html {
user-select: none;
}
-.menu li.disabled {
+.menu li.disabled{
cursor: not-allowed;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
- color: hsl(var(--bc) / 0.3);
+ color: var(--fallback-bc,oklch(var(--bc)/0.3));
}
-.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)) {
+.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)){
display: none;
}
-:where(.menu li) {
+:where(.menu li){
position: relative;
display: flex;
flex-shrink: 0;
@@ -976,11 +1192,11 @@ html {
align-items: stretch;
}
-:where(.menu li) .badge {
+:where(.menu li) .badge{
justify-self: end;
}
-.modal {
+.modal{
pointer-events: none;
position: fixed;
inset: 0px;
@@ -1003,11 +1219,11 @@ html {
overflow-y: hidden;
}
-:where(.modal) {
+:where(.modal){
align-items: center;
}
-.modal-box {
+.modal-box{
max-height: calc(100vh - 5em);
grid-column-start: 1;
grid-row-start: 1;
@@ -1016,8 +1232,12 @@ html {
--tw-scale-x: .9;
--tw-scale-y: .9;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+ border-bottom-right-radius: var(--rounded-box, 1rem);
+ border-bottom-left-radius: var(--rounded-box, 1rem);
+ border-top-left-radius: var(--rounded-box, 1rem);
+ border-top-right-radius: var(--rounded-box, 1rem);
--tw-bg-opacity: 1;
- background-color: hsl(var(--b1) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
padding: 1.5rem;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
@@ -1025,10 +1245,6 @@ html {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: 200ms;
- border-top-left-radius: var(--rounded-box, 1rem);
- border-top-right-radius: var(--rounded-box, 1rem);
- border-bottom-left-radius: var(--rounded-box, 1rem);
- border-bottom-right-radius: var(--rounded-box, 1rem);
box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px;
overflow-y: auto;
overscroll-behavior: contain;
@@ -1037,66 +1253,120 @@ html {
.modal-open,
.modal:target,
.modal-toggle:checked + .modal,
-.modal[open] {
+.modal[open]{
pointer-events: auto;
visibility: visible;
opacity: 1;
}
-.modal-action {
+.modal-action{
display: flex;
margin-top: 1.5rem;
justify-content: flex-end;
}
-:root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) {
+:root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])){
overflow: hidden;
}
-.avatar-group :where(.avatar) {
+.textarea{
+ min-height: 3rem;
+ flex-shrink: 1;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ line-height: 2;
+ border-radius: var(--rounded-btn, 0.5rem);
+ border-width: 1px;
+ border-color: transparent;
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
+}
+
+.avatar-group :where(.avatar){
overflow: hidden;
border-radius: 9999px;
border-width: 4px;
--tw-border-opacity: 1;
- border-color: hsl(var(--b1) / var(--tw-border-opacity));
+ border-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity)));
}
-.btm-nav > *:where(.active) {
+.badge-neutral{
+ --tw-border-opacity: 1;
+ border-color: var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
+ --tw-text-opacity: 1;
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
+}
+
+.badge-outline.badge-neutral{
+ --tw-text-opacity: 1;
+ color: var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity)));
+}
+
+.btm-nav > *:where(.active){
border-top-width: 2px;
--tw-bg-opacity: 1;
- background-color: hsl(var(--b1) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
-.btm-nav > * .label {
+.btm-nav > * .label{
font-size: 1rem;
line-height: 1.5rem;
}
.btn:active:hover,
- .btn:active:focus {
+ .btn:active:focus{
animation: button-pop 0s ease-out;
transform: scale(var(--btn-focus-scale, 0.97));
}
-.btn:focus-visible {
+@supports not (color: oklch(0 0 0)){
+ .btn{
+ background-color: var(--btn-color, var(--fallback-b2));
+ border-color: var(--btn-color, var(--fallback-b2));
+ }
+
+ .btn-neutral{
+ --btn-color: var(--fallback-n);
+ }
+}
+
+.btn:focus-visible{
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
}
-.btn.glass {
+@supports (color: oklch(0 0 0)){
+ .btn-neutral{
+ --btn-color: var(--n);
+ }
+}
+
+.btn-neutral{
+ --tw-text-opacity: 1;
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
+ outline-color: var(--fallback-n,oklch(var(--n)/1));
+}
+
+.btn.glass{
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
outline-color: currentColor;
}
-.btn.glass.btn-active {
+.btn.glass.btn-active{
--glass-opacity: 25%;
--glass-border-opacity: 15%;
}
-.btn-ghost {
+.btn-ghost{
border-width: 1px;
border-color: transparent;
background-color: transparent;
@@ -1107,132 +1377,116 @@ html {
outline-color: currentColor;
}
-.btn-ghost.btn-active {
- --tw-border-opacity: 0;
- background-color: hsl(var(--bc) / var(--tw-bg-opacity));
- --tw-bg-opacity: 0.2;
+.btn-ghost.btn-active{
+ border-color: transparent;
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.btn.btn-disabled,
.btn[disabled],
- .btn:disabled {
+ .btn:disabled{
--tw-border-opacity: 0;
- background-color: hsl(var(--n) / var(--tw-bg-opacity));
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-bg-opacity: 0.2;
- color: hsl(var(--bc) / var(--tw-text-opacity));
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
--tw-text-opacity: 0.2;
}
-.btn-group > input[type="radio"]:checked.btn,
- .btn-group > .btn-active {
- --tw-border-opacity: 1;
- border-color: hsl(var(--p) / var(--tw-border-opacity));
- --tw-bg-opacity: 1;
- background-color: hsl(var(--p) / var(--tw-bg-opacity));
- --tw-text-opacity: 1;
- color: hsl(var(--pc) / var(--tw-text-opacity));
-}
-
-.btn-group > input[type="radio"]:checked.btn:focus-visible, .btn-group > .btn-active:focus-visible {
- outline-style: solid;
- outline-width: 2px;
- outline-color: hsl(var(--p) / 1);
-}
-
.btn:is(input[type="checkbox"]:checked),
-.btn:is(input[type="radio"]:checked) {
+.btn:is(input[type="radio"]:checked){
--tw-border-opacity: 1;
- border-color: hsl(var(--p) / var(--tw-border-opacity));
+ border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
- background-color: hsl(var(--p) / var(--tw-bg-opacity));
+ background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
- color: hsl(var(--pc) / var(--tw-text-opacity));
+ color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
-.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible {
- outline-color: hsl(var(--p) / 1);
+.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible{
+ outline-color: var(--fallback-p,oklch(var(--p)/1));
}
-@keyframes button-pop {
- 0% {
+@keyframes button-pop{
+ 0%{
transform: scale(var(--btn-focus-scale, 0.98));
}
- 40% {
+ 40%{
transform: scale(1.02);
}
- 100% {
+ 100%{
transform: scale(1);
}
}
-@keyframes checkmark {
- 0% {
+@keyframes checkmark{
+ 0%{
background-position-y: 5px;
}
- 50% {
+ 50%{
background-position-y: -2px;
}
- 100% {
+ 100%{
background-position-y: 0;
}
}
-details.collapse {
+details.collapse{
width: 100%;
}
-details.collapse summary {
+details.collapse summary{
position: relative;
display: block;
outline: 2px solid transparent;
outline-offset: 2px;
}
-details.collapse summary::-webkit-details-marker {
+details.collapse summary::-webkit-details-marker{
display: none;
}
-.collapse:focus-visible {
+.collapse:focus-visible{
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
- outline-color: hsl(var(--bc) / 1);
+ outline-color: var(--fallback-bc,oklch(var(--bc)/1));
}
.collapse:has(.collapse-title:focus-visible),
.collapse:has(> input[type="checkbox"]:focus-visible),
-.collapse:has(> input[type="radio"]:focus-visible) {
+.collapse:has(> input[type="radio"]:focus-visible){
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
- outline-color: hsl(var(--bc) / 1);
+ outline-color: var(--fallback-bc,oklch(var(--bc)/1));
}
.collapse:not(.collapse-open):not(.collapse-close) > input[type="checkbox"],
.collapse:not(.collapse-open):not(.collapse-close) > input[type="radio"]:not(:checked),
-.collapse:not(.collapse-open):not(.collapse-close) > .collapse-title {
+.collapse:not(.collapse-open):not(.collapse-close) > .collapse-title{
cursor: pointer;
}
-.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open]) > .collapse-title {
+.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open]) > .collapse-title{
cursor: unset;
}
:where(.collapse > input[type="checkbox"]),
-:where(.collapse > input[type="radio"]) {
+:where(.collapse > input[type="radio"]){
z-index: 1;
}
.collapse-title,
:where(.collapse > input[type="checkbox"]),
-:where(.collapse > input[type="radio"]) {
+:where(.collapse > input[type="radio"]){
width: 100%;
padding: 1rem;
- padding-right: 3rem;
+ -webkit-padding-end: 3rem;
+ padding-inline-end: 3rem;
min-height: 3.75rem;
transition: background-color 0.2s ease-out;
}
@@ -1241,7 +1495,7 @@ details.collapse summary::-webkit-details-marker {
.collapse-open > :where(.collapse-content),
.collapse:focus:not(.collapse-close) > :where(.collapse-content),
.collapse:not(.collapse-close) > :where(input[type="checkbox"]:checked ~ .collapse-content),
-.collapse:not(.collapse-close) > :where(input[type="radio"]:checked ~ .collapse-content) {
+.collapse:not(.collapse-close) > :where(input[type="radio"]:checked ~ .collapse-content){
padding-bottom: 1rem;
transition: padding 0.2s ease-out,
background-color 0.2s ease-out;
@@ -1251,101 +1505,96 @@ details.collapse summary::-webkit-details-marker {
.collapse-open.collapse-arrow > .collapse-title:after,
.collapse-arrow:focus:not(.collapse-close) > .collapse-title:after,
.collapse-arrow:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after,
-.collapse-arrow:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after {
+.collapse-arrow:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after{
--tw-translate-y: -50%;
--tw-rotate: 225deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
-[dir="rtl"] .collapse[open].collapse-arrow > .collapse-title:after,
-[dir="rtl"] .collapse-open.collapse-arrow > .collapse-title:after,
-[dir="rtl"] .collapse-arrow:focus:not(.collapse-close) .collapse-title:after,
-[dir="rtl"]
- .collapse-arrow:not(.collapse-close)
- input[type="checkbox"]:checked
- ~ .collapse-title:after {
- --tw-rotate: 135deg;
-}
-
.collapse[open].collapse-plus > .collapse-title:after,
.collapse-open.collapse-plus > .collapse-title:after,
.collapse-plus:focus:not(.collapse-close) > .collapse-title:after,
.collapse-plus:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after,
-.collapse-plus:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after {
+.collapse-plus:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after{
content: "−";
}
-.divider:before {
- background-color: hsl(var(--bc) / var(--tw-bg-opacity));
- --tw-bg-opacity: 0.1;
-}
-
-.divider:after {
- background-color: hsl(var(--bc) / var(--tw-bg-opacity));
- --tw-bg-opacity: 0.1;
-}
-
-.divider:not(:empty) {
+.divider:not(:empty){
gap: 1rem;
}
.dropdown.dropdown-open .dropdown-content,
.dropdown:focus .dropdown-content,
-.dropdown:focus-within .dropdown-content {
+.dropdown:focus-within .dropdown-content{
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
-.input[list]::-webkit-calendar-picker-indicator {
+.input input:focus{
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
+.input[list]::-webkit-calendar-picker-indicator{
line-height: 1em;
}
-.input-bordered {
- --tw-border-opacity: 0.2;
+.input-bordered{
+ border-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
-.input:focus {
+.input:focus,
+ .input:focus-within{
+ box-shadow: none;
+ border-color: var(--fallback-bc,oklch(var(--bc)/0.2));
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
- outline-color: hsl(var(--bc) / 0.2);
+ outline-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.input-disabled,
.input:disabled,
- .input[disabled] {
+ .input[disabled]{
cursor: not-allowed;
--tw-border-opacity: 1;
- border-color: hsl(var(--b2) / var(--tw-border-opacity));
+ border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
- background-color: hsl(var(--b2) / var(--tw-bg-opacity));
- --tw-text-opacity: 0.2;
+ background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
+ color: var(--fallback-bc,oklch(var(--bc)/0.4));
}
-.input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder {
- color: hsl(var(--bc) / var(--tw-placeholder-opacity));
+.input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder{
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
--tw-placeholder-opacity: 0.2;
}
.input-disabled::placeholder,
.input:disabled::placeholder,
- .input[disabled]::placeholder {
- color: hsl(var(--bc) / var(--tw-placeholder-opacity));
+ .input[disabled]::placeholder{
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
--tw-placeholder-opacity: 0.2;
}
-.link:focus {
+.join > :where(*:not(:first-child)){
+ margin-top: 0px;
+ margin-bottom: 0px;
+ -webkit-margin-start: -1px;
+ margin-inline-start: -1px;
+}
+
+.link:focus{
outline: 2px solid transparent;
outline-offset: 2px;
}
-.link:focus-visible {
+.link:focus-visible{
outline: 2px solid currentColor;
outline-offset: 2px;
}
-.loading {
+.loading{
pointer-events: none;
display: inline-block;
aspect-ratio: 1 / 1;
@@ -1361,45 +1610,49 @@ details.collapse summary::-webkit-details-marker {
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
}
-.loading-spinner {
+.loading-spinner{
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
}
-.loading-lg {
+.loading-md{
+ width: 1.5rem;
+}
+
+.loading-lg{
width: 2.5rem;
}
-:where(.menu li:empty) {
- background-color: hsl(var(--bc) / 0.1);
+:where(.menu li:empty){
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
margin: 0.5rem 1rem;
height: 1px;
}
-.menu :where(li ul):before {
+.menu :where(li ul):before{
position: absolute;
- left: 0px;
- top: 0.75rem;
bottom: 0.75rem;
+ inset-inline-start: 0px;
+ top: 0.75rem;
width: 1px;
- background-color: hsl(var(--bc) / 0.1);
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
content: "";
}
.menu :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)),
-.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
+.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)){
+ border-radius: var(--rounded-btn, 0.5rem);
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
- text-align: left;
+ text-align: start;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: 200ms;
- border-radius: var(--rounded-btn, 0.5rem);
text-wrap: balance;
}
@@ -1408,30 +1661,30 @@ details.collapse summary::-webkit-details-marker {
:where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):is(summary):not(.active):focus-visible,
:where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active).focus,
:where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active):focus,
- :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):is(summary):not(.active):focus-visible {
+ :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):is(summary):not(.active):focus-visible{
cursor: pointer;
- background-color: hsl(var(--bc) / 0.1);
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
--tw-text-opacity: 1;
- color: hsl(var(--bc) / var(--tw-text-opacity));
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
outline: 2px solid transparent;
outline-offset: 2px;
}
.menu li > *:not(ul):not(.menu-title):not(details):active,
.menu li > *:not(ul):not(.menu-title):not(details).active,
-.menu li > details > summary:active {
+.menu li > details > summary:active{
--tw-bg-opacity: 1;
- background-color: hsl(var(--n) / var(--tw-bg-opacity));
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
- color: hsl(var(--nc) / var(--tw-text-opacity));
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
}
-.menu :where(li > details > summary)::-webkit-details-marker {
+.menu :where(li > details > summary)::-webkit-details-marker{
display: none;
}
.menu :where(li > details > summary):after,
-.menu :where(li > .menu-dropdown-toggle):after {
+.menu :where(li > .menu-dropdown-toggle):after{
justify-self: end;
display: block;
margin-top: -0.5rem;
@@ -1448,18 +1701,18 @@ details.collapse summary::-webkit-details-marker {
}
.menu :where(li > details[open] > summary):after,
-.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after {
+.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after{
transform: rotate(225deg);
margin-top: 0;
}
-.mockup-phone .display {
+.mockup-phone .display{
overflow: hidden;
border-radius: 40px;
margin-top: -25px;
}
-.mockup-browser .mockup-browser-toolbar .input {
+.mockup-browser .mockup-browser-toolbar .input{
position: relative;
margin-left: auto;
margin-right: auto;
@@ -1470,15 +1723,16 @@ details.collapse summary::-webkit-details-marker {
text-overflow: ellipsis;
white-space: nowrap;
--tw-bg-opacity: 1;
- background-color: hsl(var(--b2) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
padding-left: 2rem;
+ direction: ltr;
}
-.mockup-browser .mockup-browser-toolbar .input:before {
+.mockup-browser .mockup-browser-toolbar .input:before{
content: "";
position: absolute;
- top: 50%;
left: 0.5rem;
+ top: 50%;
aspect-ratio: 1 / 1;
height: 0.75rem;
--tw-translate-y: -50%;
@@ -1489,11 +1743,11 @@ details.collapse summary::-webkit-details-marker {
opacity: 0.6;
}
-.mockup-browser .mockup-browser-toolbar .input:after {
+.mockup-browser .mockup-browser-toolbar .input:after{
content: "";
position: absolute;
- top: 50%;
left: 1.25rem;
+ top: 50%;
height: 0.5rem;
--tw-translate-y: 25%;
--tw-rotate: -45deg;
@@ -1505,7 +1759,7 @@ details.collapse summary::-webkit-details-marker {
}
.modal:not(dialog:not(.modal-open)),
- .modal::backdrop {
+ .modal::backdrop{
background-color: rgba(0, 0, 0, 0.3);
animation: modal-pop 0.2s ease-out;
}
@@ -1513,278 +1767,340 @@ details.collapse summary::-webkit-details-marker {
.modal-open .modal-box,
.modal-toggle:checked + .modal .modal-box,
.modal:target .modal-box,
-.modal[open] .modal-box {
+.modal[open] .modal-box{
--tw-translate-y: 0px;
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
-.modal-action > :not([hidden]) ~ :not([hidden]) {
+.modal-action > :not([hidden]) ~ :not([hidden]){
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
-@keyframes modal-pop {
- 0% {
+@keyframes modal-pop{
+ 0%{
opacity: 0;
}
}
-@keyframes progress-loading {
- 50% {
+@keyframes progress-loading{
+ 50%{
background-position-x: -115%;
}
}
-@keyframes radiomark {
- 0% {
- box-shadow: 0 0 0 12px hsl(var(--b1)) inset,
- 0 0 0 12px hsl(var(--b1)) inset;
+@keyframes radiomark{
+ 0%{
+ box-shadow: 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,
+ 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset;
}
- 50% {
- box-shadow: 0 0 0 3px hsl(var(--b1)) inset,
- 0 0 0 3px hsl(var(--b1)) inset;
+ 50%{
+ box-shadow: 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,
+ 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset;
}
- 100% {
- box-shadow: 0 0 0 4px hsl(var(--b1)) inset,
- 0 0 0 4px hsl(var(--b1)) inset;
+ 100%{
+ box-shadow: 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,
+ 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset;
}
}
-@keyframes rating-pop {
- 0% {
+@keyframes rating-pop{
+ 0%{
transform: translateY(-0.125em);
}
- 40% {
+ 40%{
transform: translateY(-0.125em);
}
- 100% {
+ 100%{
transform: translateY(0);
}
}
+@keyframes skeleton{
+ from{
+ background-position: 150%;
+ }
+
+ to{
+ background-position: -50%;
+ }
+}
+
.table tr.active,
.table tr.active:nth-child(even),
- .table-zebra tbody tr:nth-child(even) {
+ .table-zebra tbody tr:nth-child(even){
--tw-bg-opacity: 1;
- background-color: hsl(var(--b2) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}
.table-zebra tr.active,
.table-zebra tr.active:nth-child(even),
- .table-zebra-zebra tbody tr:nth-child(even) {
+ .table-zebra-zebra tbody tr:nth-child(even){
--tw-bg-opacity: 1;
- background-color: hsl(var(--b3) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
}
-@keyframes toast-pop {
- 0% {
+.textarea-bordered{
+ border-color: var(--fallback-bc,oklch(var(--bc)/0.2));
+}
+
+.textarea:focus{
+ box-shadow: none;
+ border-color: var(--fallback-bc,oklch(var(--bc)/0.2));
+ outline-style: solid;
+ outline-width: 2px;
+ outline-offset: 2px;
+ outline-color: var(--fallback-bc,oklch(var(--bc)/0.2));
+}
+
+.textarea-disabled,
+ .textarea:disabled,
+ .textarea[disabled]{
+ cursor: not-allowed;
+ --tw-border-opacity: 1;
+ border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
+ --tw-text-opacity: 0.2;
+}
+
+.textarea-disabled::-moz-placeholder, .textarea:disabled::-moz-placeholder, .textarea[disabled]::-moz-placeholder{
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
+ --tw-placeholder-opacity: 0.2;
+}
+
+.textarea-disabled::placeholder,
+ .textarea:disabled::placeholder,
+ .textarea[disabled]::placeholder{
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
+ --tw-placeholder-opacity: 0.2;
+}
+
+@keyframes toast-pop{
+ 0%{
transform: scale(0.9);
opacity: 0;
}
- 100% {
+ 100%{
transform: scale(1);
opacity: 1;
}
}
-.rounded-box {
- border-radius: var(--rounded-box, 1rem);
+.no-animation{
+ --btn-focus-scale: 1;
+ --animation-btn: 0;
+ --animation-input: 0;
}
-.btm-nav-xs > *:where(.active) {
+.btm-nav-xs > *:where(.active){
border-top-width: 1px;
}
-.btm-nav-sm > *:where(.active) {
+.btm-nav-sm > *:where(.active){
border-top-width: 2px;
}
-.btm-nav-md > *:where(.active) {
+.btm-nav-md > *:where(.active){
border-top-width: 2px;
}
-.btm-nav-lg > *:where(.active) {
+.btm-nav-lg > *:where(.active){
border-top-width: 4px;
}
-.btn-xs {
+.btn-xs{
height: 1.5rem;
+ min-height: 1.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
- min-height: 1.5rem;
font-size: 0.75rem;
}
-.btn-sm {
+.btn-sm{
height: 2rem;
+ min-height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
- min-height: 2rem;
font-size: 0.875rem;
}
-.btn-square:where(.btn-xs) {
+.btn-square:where(.btn-xs){
height: 1.5rem;
width: 1.5rem;
padding: 0px;
}
-.btn-square:where(.btn-sm) {
+.btn-square:where(.btn-sm){
height: 2rem;
width: 2rem;
padding: 0px;
}
-.btn-circle:where(.btn-xs) {
+.btn-circle:where(.btn-xs){
height: 1.5rem;
width: 1.5rem;
border-radius: 9999px;
padding: 0px;
}
-.btn-circle:where(.btn-sm) {
+.btn-circle:where(.btn-sm){
height: 2rem;
width: 2rem;
border-radius: 9999px;
padding: 0px;
}
-.btn-circle:where(.btn-md) {
+.btn-circle:where(.btn-md){
height: 3rem;
width: 3rem;
border-radius: 9999px;
padding: 0px;
}
-.btn-circle:where(.btn-lg) {
+.btn-circle:where(.btn-lg){
height: 4rem;
width: 4rem;
border-radius: 9999px;
padding: 0px;
}
-.modal-bottom {
+.divider-horizontal{
+ flex-direction: column;
+}
+
+.divider-horizontal:before{
+ height: 100%;
+ width: 0.125rem;
+}
+
+.divider-horizontal:after{
+ height: 100%;
+ width: 0.125rem;
+}
+
+.join.join-vertical{
+ flex-direction: column;
+}
+
+.join.join-vertical .join-item:first-child:not(:last-child),
+ .join.join-vertical *:first-child:not(:last-child) .join-item{
+ border-end-start-radius: 0;
+ border-end-end-radius: 0;
+ border-start-start-radius: inherit;
+ border-start-end-radius: inherit;
+}
+
+.join.join-vertical .join-item:last-child:not(:first-child),
+ .join.join-vertical *:last-child:not(:first-child) .join-item{
+ border-start-start-radius: 0;
+ border-start-end-radius: 0;
+ border-end-start-radius: inherit;
+ border-end-end-radius: inherit;
+}
+
+.join.join-horizontal{
+ flex-direction: row;
+}
+
+.join.join-horizontal .join-item:first-child:not(:last-child),
+ .join.join-horizontal *:first-child:not(:last-child) .join-item{
+ border-end-end-radius: 0;
+ border-start-end-radius: 0;
+ border-end-start-radius: inherit;
+ border-start-start-radius: inherit;
+}
+
+.join.join-horizontal .join-item:last-child:not(:first-child),
+ .join.join-horizontal *:last-child:not(:first-child) .join-item{
+ border-end-start-radius: 0;
+ border-start-start-radius: 0;
+ border-end-end-radius: inherit;
+ border-start-end-radius: inherit;
+}
+
+.modal-bottom{
place-items: end;
}
-.avatar.online:before {
+.avatar.online:before{
content: "";
position: absolute;
z-index: 10;
display: block;
border-radius: 9999px;
--tw-bg-opacity: 1;
- background-color: hsl(var(--su) / var(--tw-bg-opacity));
+ background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)));
outline-style: solid;
outline-width: 2px;
- outline-color: hsl(var(--b1) / 1);
+ outline-color: var(--fallback-b1,oklch(var(--b1)/1));
width: 15%;
height: 15%;
top: 7%;
right: 7%;
}
-.avatar.offline:before {
+.avatar.offline:before{
content: "";
position: absolute;
z-index: 10;
display: block;
border-radius: 9999px;
--tw-bg-opacity: 1;
- background-color: hsl(var(--b3) / var(--tw-bg-opacity));
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
outline-style: solid;
outline-width: 2px;
- outline-color: hsl(var(--b1) / 1);
+ outline-color: var(--fallback-b1,oklch(var(--b1)/1));
width: 15%;
height: 15%;
top: 7%;
right: 7%;
}
-.btn-group .btn:not(:first-child):not(:last-child) {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
+.divider-horizontal{
+ margin-left: 1rem;
+ margin-right: 1rem;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ height: auto;
+ width: 1rem;
}
-.btn-group .btn:first-child:not(:last-child) {
- margin-top: -0px;
- margin-left: -1px;
- border-top-left-radius: var(--rounded-btn, 0.5rem);
- border-top-right-radius: 0;
- border-bottom-left-radius: var(--rounded-btn, 0.5rem);
- border-bottom-right-radius: 0;
-}
-
-.btn-group .btn:last-child:not(:first-child) {
- border-top-left-radius: 0;
- border-top-right-radius: var(--rounded-btn, 0.5rem);
- border-bottom-left-radius: 0;
- border-bottom-right-radius: var(--rounded-btn, 0.5rem);
-}
-
-.btn-group-horizontal .btn:not(:first-child):not(:last-child) {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-
-.btn-group-horizontal .btn:first-child:not(:last-child) {
- margin-top: -0px;
- margin-left: -1px;
- border-top-left-radius: var(--rounded-btn, 0.5rem);
- border-top-right-radius: 0;
- border-bottom-left-radius: var(--rounded-btn, 0.5rem);
- border-bottom-right-radius: 0;
-}
-
-.btn-group-horizontal .btn:last-child:not(:first-child) {
- border-top-left-radius: 0;
- border-top-right-radius: var(--rounded-btn, 0.5rem);
- border-bottom-left-radius: 0;
- border-bottom-right-radius: var(--rounded-btn, 0.5rem);
-}
-
-.btn-group-vertical .btn:first-child:not(:last-child) {
+.join.join-vertical > :where(*:not(:first-child)){
+ margin-left: 0px;
+ margin-right: 0px;
margin-top: -1px;
- margin-left: -0px;
- border-top-left-radius: var(--rounded-btn, 0.5rem);
- border-top-right-radius: var(--rounded-btn, 0.5rem);
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
}
-.btn-group-vertical .btn:last-child:not(:first-child) {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- border-bottom-left-radius: var(--rounded-btn, 0.5rem);
- border-bottom-right-radius: var(--rounded-btn, 0.5rem);
+.join.join-horizontal > :where(*:not(:first-child)){
+ margin-top: 0px;
+ margin-bottom: 0px;
+ -webkit-margin-start: -1px;
+ margin-inline-start: -1px;
}
-.modal-top :where(.modal-box) {
+.modal-top :where(.modal-box){
width: 100%;
max-width: none;
--tw-translate-y: -2.5rem;
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+ border-bottom-right-radius: var(--rounded-box, 1rem);
+ border-bottom-left-radius: var(--rounded-box, 1rem);
border-top-left-radius: 0px;
border-top-right-radius: 0px;
- border-bottom-left-radius: var(--rounded-box, 1rem);
- border-bottom-right-radius: var(--rounded-box, 1rem);
}
-.modal-middle :where(.modal-box) {
+.modal-middle :where(.modal-box){
width: 91.666667%;
max-width: 32rem;
--tw-translate-y: 0px;
@@ -1793,347 +2109,719 @@ details.collapse summary::-webkit-details-marker {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
border-top-left-radius: var(--rounded-box, 1rem);
border-top-right-radius: var(--rounded-box, 1rem);
- border-bottom-left-radius: var(--rounded-box, 1rem);
border-bottom-right-radius: var(--rounded-box, 1rem);
+ border-bottom-left-radius: var(--rounded-box, 1rem);
}
-.modal-bottom :where(.modal-box) {
+.modal-bottom :where(.modal-box){
width: 100%;
max-width: none;
--tw-translate-y: 2.5rem;
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
- border-bottom-right-radius: 0px;
- border-bottom-left-radius: 0px;
border-top-left-radius: var(--rounded-box, 1rem);
border-top-right-radius: var(--rounded-box, 1rem);
+ border-bottom-right-radius: 0px;
+ border-bottom-left-radius: 0px;
}
-.pointer-events-none {
+.pointer-events-none{
pointer-events: none;
}
-.pointer-events-auto {
+.pointer-events-auto{
pointer-events: auto;
}
-.visible {
+.visible{
visibility: visible;
}
-.collapse {
+.collapse{
visibility: collapse;
}
-.fixed {
+.fixed{
position: fixed;
}
-.absolute {
+.absolute{
position: absolute;
}
-.relative {
+.relative{
position: relative;
}
-.bottom-0 {
+.-left-\[100px\]{
+ left: -100px;
+}
+
+.bottom-0{
bottom: 0px;
}
-.bottom-8 {
+.bottom-8{
bottom: 2rem;
}
-.left-0 {
+.left-0{
left: 0px;
}
-.left-1\/2 {
+.left-1\/2{
left: 50%;
}
-.left-2 {
- left: 0.5rem;
+.left-28{
+ left: 7rem;
}
-.right-0 {
+.left-4{
+ left: 1rem;
+}
+
+.right-0{
right: 0px;
}
-.right-2 {
+.right-2{
right: 0.5rem;
}
-.top-0 {
+.right-4{
+ right: 1rem;
+}
+
+.top-0{
top: 0px;
}
-.top-1\/2 {
+.top-1\/2{
top: 50%;
}
-.top-2 {
+.top-2{
top: 0.5rem;
}
-.z-\[1000\] {
+.top-4{
+ top: 1rem;
+}
+
+.z-\[1000\]{
z-index: 1000;
}
-.z-\[100\] {
+.z-\[1001\]{
+ z-index: 1001;
+}
+
+.z-\[100\]{
z-index: 100;
}
-.z-\[200\] {
+.z-\[10\]{
+ z-index: 10;
+}
+
+.z-\[200\]{
z-index: 200;
}
-.my-0 {
+.z-\[99\]{
+ z-index: 99;
+}
+
+.m-0{
+ margin: 0px;
+}
+
+.\!my-0{
+ margin-top: 0px !important;
+ margin-bottom: 0px !important;
+}
+
+.my-0{
margin-top: 0px;
margin-bottom: 0px;
}
-.mb-2 {
+.mb-2{
margin-bottom: 0.5rem;
}
-.ml-2 {
+.ml-2{
margin-left: 0.5rem;
}
-.mr-4 {
+.ml-4{
+ margin-left: 1rem;
+}
+
+.mr-4{
margin-right: 1rem;
}
-.mt-2 {
+.mt-2{
margin-top: 0.5rem;
}
-.mt-4 {
+.mt-4{
margin-top: 1rem;
}
-.flex {
+.mt-8{
+ margin-top: 2rem;
+}
+
+.block{
+ display: block;
+}
+
+.flex{
display: flex;
}
-.hidden {
+.hidden{
display: none;
}
-.h-3 {
+.h-3{
height: 0.75rem;
}
-.h-\[370px\] {
- height: 370px;
+.h-36{
+ height: 9rem;
}
-.h-full {
+.h-96{
+ height: 24rem;
+}
+
+.h-\[394px\]{
+ height: 394px;
+}
+
+.h-fit{
+ height: -moz-fit-content;
+ height: fit-content;
+}
+
+.h-full{
height: 100%;
}
-.w-3 {
+.min-h-\[400px\]{
+ min-height: 400px;
+}
+
+.w-20{
+ width: 5rem;
+}
+
+.w-3{
width: 0.75rem;
}
-.w-56 {
+.w-56{
width: 14rem;
}
-.w-\[320px\] {
- width: 320px;
+.w-96{
+ width: 24rem;
}
-.w-fit {
+.w-\[24rem\]{
+ width: 24rem;
+}
+
+.w-\[32rem\]{
+ width: 32rem;
+}
+
+.w-fit{
width: -moz-fit-content;
width: fit-content;
}
-.w-full {
+.w-full{
width: 100%;
}
-.-translate-x-1\/2 {
+.min-w-\[400px\]{
+ min-width: 400px;
+}
+
+.max-w-\[100\%\]{
+ max-width: 100%;
+}
+
+.-translate-x-1\/2{
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
-.-translate-y-1\/2 {
+.-translate-y-1\/2{
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
-.transform {
+.transform{
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
-.flex-row {
+.resize-none{
+ resize: none;
+}
+
+.flex-row{
flex-direction: row;
}
-.flex-col {
+.flex-col{
flex-direction: column;
}
-.items-start {
+.items-start{
align-items: flex-start;
}
-.items-center {
+.items-center{
align-items: center;
}
-.justify-center {
+.justify-start{
+ justify-content: flex-start;
+}
+
+.justify-center{
justify-content: center;
}
-.justify-between {
+.justify-between{
justify-content: space-between;
}
-.gap-2 {
+.justify-around{
+ justify-content: space-around;
+}
+
+.gap-2{
gap: 0.5rem;
}
-.gap-4 {
+.gap-4{
gap: 1rem;
}
-.place-self-end {
+.place-self-end{
place-self: end;
}
-.rounded-2xl {
- border-radius: 1rem;
+.overflow-hidden{
+ overflow: hidden;
}
-.rounded-full {
+.overflow-y-auto{
+ overflow-y: auto;
+}
+
+.overflow-x-hidden{
+ overflow-x: hidden;
+}
+
+.rounded{
+ border-radius: 0.25rem;
+}
+
+.rounded-box{
+ border-radius: var(--rounded-box, 1rem);
+}
+
+.rounded-full{
border-radius: 9999px;
}
-.rounded-md {
+.rounded-lg{
+ border-radius: 0.5rem;
+}
+
+.rounded-md{
border-radius: 0.375rem;
}
-.rounded-sm {
+.rounded-sm{
border-radius: 0.125rem;
}
-.border-t-4 {
+.rounded-b-md{
+ border-bottom-right-radius: 0.375rem;
+ border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-b-none{
+ border-bottom-right-radius: 0px;
+ border-bottom-left-radius: 0px;
+}
+
+.rounded-l-md{
+ border-top-left-radius: 0.375rem;
+ border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-l-none{
+ border-top-left-radius: 0px;
+ border-bottom-left-radius: 0px;
+}
+
+.rounded-r-md{
+ border-top-right-radius: 0.375rem;
+ border-bottom-right-radius: 0.375rem;
+}
+
+.rounded-r-none{
+ border-top-right-radius: 0px;
+ border-bottom-right-radius: 0px;
+}
+
+.rounded-t-md{
+ border-top-left-radius: 0.375rem;
+ border-top-right-radius: 0.375rem;
+}
+
+.border{
+ border-width: 1px;
+}
+
+.border-2{
+ border-width: 2px;
+}
+
+.border-b-0{
+ border-bottom-width: 0px;
+}
+
+.border-t-4{
border-top-width: 4px;
}
-.border-none {
+.border-dashed{
+ border-style: dashed;
+}
+
+.border-none{
border-style: none;
}
-.border-orange-500 {
+.border-black{
+ --tw-border-opacity: 1;
+ border-color: rgb(0 0 0 / var(--tw-border-opacity));
+}
+
+.border-blue-500{
+ --tw-border-opacity: 1;
+ border-color: rgb(59 130 246 / var(--tw-border-opacity));
+}
+
+.border-gray-300{
+ --tw-border-opacity: 1;
+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
+}
+
+.border-green-500{
+ --tw-border-opacity: 1;
+ border-color: rgb(34 197 94 / var(--tw-border-opacity));
+}
+
+.border-orange-500{
--tw-border-opacity: 1;
border-color: rgb(249 115 22 / var(--tw-border-opacity));
}
-.bg-base-200 {
- --tw-bg-opacity: 1;
- background-color: hsl(var(--b2) / var(--tw-bg-opacity));
+.border-purple-500{
+ --tw-border-opacity: 1;
+ border-color: rgb(168 85 247 / var(--tw-border-opacity));
}
-.bg-black {
+.border-red-500{
+ --tw-border-opacity: 1;
+ border-color: rgb(239 68 68 / var(--tw-border-opacity));
+}
+
+.bg-base-200{
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
+}
+
+.bg-black{
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
-.bg-gray-900 {
+.bg-blue-100{
+ --tw-bg-opacity: 1;
+ background-color: rgb(219 234 254 / var(--tw-bg-opacity));
+}
+
+.bg-gray-900{
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
-.bg-green-500 {
+.bg-green-100{
+ --tw-bg-opacity: 1;
+ background-color: rgb(220 252 231 / var(--tw-bg-opacity));
+}
+
+.bg-green-500{
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
-.bg-orange-100 {
+.bg-neutral{
+ --tw-bg-opacity: 1;
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
+}
+
+.bg-neutral-800{
+ --tw-bg-opacity: 1;
+ background-color: rgb(38 38 38 / var(--tw-bg-opacity));
+}
+
+.bg-orange-100{
--tw-bg-opacity: 1;
background-color: rgb(255 237 213 / var(--tw-bg-opacity));
}
-.bg-red-500 {
+.bg-red-100{
+ --tw-bg-opacity: 1;
+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
+}
+
+.bg-red-500{
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
-.bg-opacity-50 {
+.bg-transparent{
+ background-color: transparent;
+}
+
+.bg-white{
+ --tw-bg-opacity: 1;
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
+}
+
+.bg-opacity-50{
--tw-bg-opacity: 0.5;
}
-.p-0 {
+.bg-gradient-to-b{
+ background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
+}
+
+.from-black{
+ --tw-gradient-from: #000 var(--tw-gradient-from-position);
+ --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
+}
+
+.from-gray-500{
+ --tw-gradient-from: #6b7280 var(--tw-gradient-from-position);
+ --tw-gradient-to: rgb(107 114 128 / 0) var(--tw-gradient-to-position);
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
+}
+
+.via-\[\#5F5F5F\]{
+ --tw-gradient-to: rgb(95 95 95 / 0) var(--tw-gradient-to-position);
+ --tw-gradient-stops: var(--tw-gradient-from), #5F5F5F var(--tw-gradient-via-position), var(--tw-gradient-to);
+}
+
+.via-50\%{
+ --tw-gradient-via-position: 50%;
+}
+
+.via-60\%{
+ --tw-gradient-via-position: 60%;
+}
+
+.to-white{
+ --tw-gradient-to: #fff var(--tw-gradient-to-position);
+}
+
+.bg-clip-text{
+ -webkit-background-clip: text;
+ background-clip: text;
+}
+
+.object-contain{
+ -o-object-fit: contain;
+ object-fit: contain;
+}
+
+.p-0{
padding: 0px;
}
-.p-2 {
+.p-2{
padding: 0.5rem;
}
-.\!py-0 {
+.p-24{
+ padding: 6rem;
+}
+
+.p-4{
+ padding: 1rem;
+}
+
+.\!px-0{
+ padding-left: 0px !important;
+ padding-right: 0px !important;
+}
+
+.\!py-0{
padding-top: 0px !important;
padding-bottom: 0px !important;
}
-.px-4 {
+.px-4{
padding-left: 1rem;
padding-right: 1rem;
}
-.py-2 {
+.py-16{
+ padding-top: 4rem;
+ padding-bottom: 4rem;
+}
+
+.py-2{
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
-.text-start {
+.text-start{
text-align: start;
}
-.text-lg {
+.font-gabarito{
+ font-family: Gabarito;
+}
+
+.text-2xl{
+ font-size: 1.5rem;
+ line-height: 2rem;
+}
+
+.text-4xl{
+ font-size: 2.25rem;
+ line-height: 2.5rem;
+}
+
+.text-lg{
font-size: 1.125rem;
line-height: 1.75rem;
}
-.text-xl {
+.text-xl{
font-size: 1.25rem;
line-height: 1.75rem;
}
-.font-bold {
+.text-xs{
+ font-size: 0.75rem;
+ line-height: 1rem;
+}
+
+.font-bold{
font-weight: 700;
}
-.normal-case {
+.normal-case{
text-transform: none;
}
-.text-base-content {
- --tw-text-opacity: 1;
- color: hsl(var(--bc) / var(--tw-text-opacity));
+.italic{
+ font-style: italic;
}
-.text-black {
+.text-base-content{
+ --tw-text-opacity: 1;
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
+}
+
+.text-black{
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
-.text-orange-700 {
+.text-blue-700{
+ --tw-text-opacity: 1;
+ color: rgb(29 78 216 / var(--tw-text-opacity));
+}
+
+.text-gray-300{
+ --tw-text-opacity: 1;
+ color: rgb(209 213 219 / var(--tw-text-opacity));
+}
+
+.text-gray-500{
+ --tw-text-opacity: 1;
+ color: rgb(107 114 128 / var(--tw-text-opacity));
+}
+
+.text-green-700{
+ --tw-text-opacity: 1;
+ color: rgb(21 128 61 / var(--tw-text-opacity));
+}
+
+.text-orange-700{
--tw-text-opacity: 1;
color: rgb(194 65 12 / var(--tw-text-opacity));
}
-.opacity-80 {
+.text-purple-500{
+ --tw-text-opacity: 1;
+ color: rgb(168 85 247 / var(--tw-text-opacity));
+}
+
+.text-red-700{
+ --tw-text-opacity: 1;
+ color: rgb(185 28 28 / var(--tw-text-opacity));
+}
+
+.text-transparent{
+ color: transparent;
+}
+
+.text-white{
+ --tw-text-opacity: 1;
+ color: rgb(255 255 255 / var(--tw-text-opacity));
+}
+
+.opacity-80{
opacity: 0.8;
}
-.filter {
+.shadow{
+ --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.\!outline-none{
+ outline: 2px solid transparent !important;
+ outline-offset: 2px !important;
+}
+
+.blur{
+ --tw-blur: blur(8px);
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
+}
+
+.filter{
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
@@ -2276,12 +2964,41 @@ details.collapse summary::-webkit-details-marker {
font-size: 12px;
}
-@media (min-width: 640px) {
- .sm\:modal-middle {
+img {
+ display: none;
+}
+
+img[src] {
+ display: block;
+}
+
+@media (min-width: 640px){
+ .sm\:btn-md{
+ height: 3rem;
+ min-height: 3rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ font-size: 0.875rem;
+ }
+
+ .btn-square:where(.sm\:btn-md){
+ height: 3rem;
+ width: 3rem;
+ padding: 0px;
+ }
+
+ .btn-circle:where(.sm\:btn-md){
+ height: 3rem;
+ width: 3rem;
+ border-radius: 9999px;
+ padding: 0px;
+ }
+
+ .sm\:modal-middle{
place-items: center;
}
- .sm\:modal-middle :where(.modal-box) {
+ .sm\:modal-middle :where(.modal-box){
width: 91.666667%;
max-width: 32rem;
--tw-translate-y: 0px;
@@ -2290,12 +3007,36 @@ details.collapse summary::-webkit-details-marker {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
border-top-left-radius: var(--rounded-box, 1rem);
border-top-right-radius: var(--rounded-box, 1rem);
- border-bottom-left-radius: var(--rounded-box, 1rem);
border-bottom-right-radius: var(--rounded-box, 1rem);
+ border-bottom-left-radius: var(--rounded-box, 1rem);
}
}
-.hover\:text-red-500:hover {
+.hover\:cursor-pointer:hover{
+ cursor: pointer;
+}
+
+.hover\:bg-neutral-950:hover{
+ --tw-bg-opacity: 1;
+ background-color: rgb(10 10 10 / var(--tw-bg-opacity));
+}
+
+.hover\:text-red-500:hover{
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
+
+.focus\:outline-none:focus{
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
+@media (min-width: 640px){
+ .sm\:flex{
+ display: flex;
+ }
+
+ .sm\:w-32{
+ width: 8rem;
+ }
+}
diff --git a/package.json b/package.json
index 59a6719..91641bb 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"license": "ISC",
"devDependencies": {
"chokidar": "^3.5.3",
- "daisyui": "^3.7.5",
+ "daisyui": "^4.0.7",
"tailwindcss": "^3.3.3"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ebd4edd..4721abd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,8 +9,8 @@ devDependencies:
specifier: ^3.5.3
version: 3.5.3
daisyui:
- specifier: ^3.7.5
- version: 3.7.5
+ specifier: ^4.0.7
+ version: 4.0.7(postcss@8.4.29)
tailwindcss:
specifier: ^3.3.3
version: 3.3.3
@@ -132,10 +132,6 @@ packages:
fsevents: 2.3.3
dev: true
- /colord@2.9.3:
- resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
- dev: true
-
/commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@@ -158,17 +154,21 @@ packages:
hasBin: true
dev: true
- /daisyui@3.7.5:
- resolution: {integrity: sha512-udhiBJYVvcPGXa+mL5IElke6EdddKecjbbz6m43+9IVqzK8GBwetg092Edclo42TkNboLD9nzodeesJygqZt2A==}
+ /culori@3.2.0:
+ resolution: {integrity: sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /daisyui@4.0.7(postcss@8.4.29):
+ resolution: {integrity: sha512-D84DnNDZKcamwNsxCMrwYaddyz5kC6VO6oe30nM1x67GzCAfarfd3Ar1rpLGXCIqSsEoNZUHO8EcXvX93W2ZkA==}
engines: {node: '>=16.9.0'}
dependencies:
- colord: 2.9.3
css-selector-tokenizer: 0.8.0
- postcss: 8.4.29
+ culori: 3.2.0
+ picocolors: 1.0.0
postcss-js: 4.0.1(postcss@8.4.29)
- tailwindcss: 3.3.3
transitivePeerDependencies:
- - ts-node
+ - postcss
dev: true
/didyoumean@1.2.2:
diff --git a/requirements.txt b/requirements.txt
index 0ea6151..7a88a92 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,4 +6,6 @@ einops
bpy
segment-anything
tqdm
+python-dotenv
+mediapipe
# -e git+https://github.com/facebookresearch/segment-anything.git#egg=segment_anything
\ No newline at end of file
diff --git a/routes.py b/routes.py
index 9921209..1c0161f 100644
--- a/routes.py
+++ b/routes.py
@@ -1,6 +1,7 @@
from aiohttp import web
from segment_anything import sam_model_registry, SamPredictor
from PIL import Image, ImageOps
+from dotenv import load_dotenv
import os
import requests
import folder_paths
@@ -9,6 +10,14 @@ import numpy as np
import server
import re
import base64
+from PIL import Image
+import io
+import time
+import execution
+import random
+
+load_dotenv()
+
# For speeding up ONNX model, see https://github.com/facebookresearch/segment-anything/tree/main/demo#onnx-multithreading-with-sharedarraybuffer
def inject_headers(original_handler):
@@ -33,11 +42,13 @@ for item in server.PromptServer.instance.routes._items:
routes.append(item)
server.PromptServer.instance.routes._items = routes
+
@server.PromptServer.instance.routes.get("/avatar-graph-comfyui/tw-styles.css")
async def get_web_styles(request):
filename = os.path.join(os.path.dirname(__file__), "js/tw-styles.css")
return web.FileResponse(filename)
+
@server.PromptServer.instance.routes.get("/sam_model")
async def get_sam_model(request):
model_type = request.rel_url.query.get("type", "vit_h")
@@ -55,8 +66,11 @@ async def get_sam_model(request):
return web.FileResponse(filename)
-def load_image(image):
- image_path = folder_paths.get_annotated_filepath(image)
+def load_image(image, is_generated_image):
+ if is_generated_image:
+ image_path = f"{folder_paths.get_output_directory()}/{image}"
+ else:
+ image_path = folder_paths.get_annotated_filepath(image)
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
@@ -67,58 +81,344 @@ def load_image(image):
@server.PromptServer.instance.routes.post("/sam_model")
async def post_sam_model(request):
post = await request.json()
+ is_generated_image = post.get("isGeneratedImage")
emb_id = post.get("embedding_id")
ckpt = post.get("ckpt")
ckpt = folder_paths.get_full_path("sams", ckpt)
- model_type = re.findall(r'vit_[lbh]', ckpt)[0]
+ remote = post.get("remote")
+ model_type = re.findall(r"vit_[lbh]", ckpt)[0]
emb_filename = f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.npy"
+ output_json_filename = (
+ f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.json"
+ )
if not os.path.exists(emb_filename):
- image = load_image(post.get("image"))
-
- sam = sam_model_registry[model_type](checkpoint=ckpt)
- predictor = SamPredictor(sam)
-
- image_np = (image * 255).astype(np.uint8)
- predictor.set_image(image_np)
- emb = predictor.get_image_embedding().cpu().numpy()
- np.save(emb_filename, emb)
- with open(f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.json", "w") as f:
- json.dump(
- {
- "input_size": predictor.input_size,
- "original_size": predictor.original_size,
+ image = load_image(post.get("image"), is_generated_image)
+ if remote:
+ # Run embed in remote server
+ image = Image.fromarray((image * 255).astype(np.uint8))
+ buffered = io.BytesIO()
+ image.save(buffered, format="PNG")
+ image = base64.b64encode(buffered.getvalue()).decode()
+ res = requests.post(
+ "https://avatechgg--sam-embed.modal.run",
+ headers={
+ "Content-type": "application/json",
+ "Accept": "application/json",
},
- f,
+ data=json.dumps(
+ {
+ "image": image,
+ }
+ ),
+ ).json()
+ emb, input_size, original_size = (
+ res["emb"],
+ res["input_size"],
+ res["original_size"],
)
+ emb = np.array(emb).astype(np.float32)
+ np.save(emb_filename, emb)
+ with open(output_json_filename, "w") as f:
+ data = {
+ "input_size": input_size,
+ "original_size": original_size,
+ }
+ json.dump(data, f)
+ else:
+ sam = sam_model_registry[model_type](checkpoint=ckpt).to("cuda")
+ predictor = SamPredictor(sam)
+
+ image_np = (image * 255).astype(np.uint8)
+ predictor.set_image(image_np)
+ emb = predictor.get_image_embedding().cpu().numpy()
+ np.save(emb_filename, emb)
+ with open(output_json_filename, "w") as f:
+ json.dump(
+ {
+ "input_size": predictor.input_size,
+ "original_size": predictor.original_size,
+ },
+ f,
+ )
+ print("Finished embedding")
return web.json_response({})
-@server.PromptServer.instance.routes.get("/get_default_workflow")
-async def get_default_workflow(request):
- json_link = "https://cdn.discordapp.com/attachments/1119102674437156984/1172255632586448987/workflow_boy_2_1.json?ex=655fa722&is=654d3222&hm=463fa6a3c6ea60f7471196ff45382c729d3b856e86282f905d37a0398711860e&"
- response = requests.get(json_link)
- response.raise_for_status()
- return web.json_response(response.json())
+
+def save_image(image, save_name=None):
+ input_folder = folder_paths.get_input_directory()
+ name, extension = os.path.splitext(image.filename)
+
+ if save_name == None:
+ save_name = f"{name}{extension}"
+ i = 1
+ while os.path.exists(f"{input_folder}/{save_name}"):
+ save_name = f"{name}_{i}{extension}"
+ i += 1
+
+ with open(f"{input_folder}/{save_name}", "wb") as f:
+ f.write(image.file.read())
+
+ return save_name
+
+
+def post_prompt(json_data):
+ prompt_server = server.PromptServer.instance
+ json_data = prompt_server.trigger_on_prompt(json_data)
+
+ if "number" in json_data:
+ number = float(json_data["number"])
+ else:
+ number = prompt_server.number
+ if "front" in json_data:
+ if json_data["front"]:
+ number = -number
+
+ prompt_server.number += 1
+
+ if "prompt" in json_data:
+ prompt = json_data["prompt"]
+ valid = execution.validate_prompt(prompt)
+ extra_data = {}
+ if "extra_data" in json_data:
+ extra_data = json_data["extra_data"]
+
+ if "client_id" in json_data:
+ extra_data["client_id"] = json_data["client_id"]
+ if valid[0]:
+ prompt_id = str(uuid.uuid4())
+ outputs_to_execute = valid[2]
+ prompt_server.prompt_queue.put(
+ (number, prompt_id, prompt, extra_data, outputs_to_execute)
+ )
+ response = {
+ "prompt_id": prompt_id,
+ "number": number,
+ "node_errors": valid[3],
+ }
+ return web.json_response(response)
+ else:
+ print("invalid prompt:", valid[1])
+ return web.json_response(
+ {"error": valid[1], "node_errors": valid[3]}, status=400
+ )
+ else:
+ return web.json_response({"error": "no prompt", "node_errors": []}, status=400)
+
+
+def get_avatar_file(outputs):
+ for node_id, output in outputs.items():
+ if "gltfFilename" in output:
+ avatar_filename = output["gltfFilename"][0]
+ with open(
+ f"{folder_paths.get_output_directory()}/{avatar_filename}", "rb"
+ ) as f:
+ return f.read()
+
+
+def upload_avatar_file(outputs):
+ file = get_avatar_file(outputs)
+ response = requests.get("https://labs.avatech.ai/api/share")
+ labData = response.json()
+ modelId = labData["modelId"]
+
+ # upload model
+ headers = {
+ "x-amz-acl": "public-read",
+ "Content-Type": "model/gltf-binary",
+ "Content-Length": str(len(file)),
+ }
+ requests.put(labData["url"], headers=headers, data=file)
+
+ # send notification
+ webhook_url = os.getenv("DISCORD_WEBHOOK_URL")
+ data = {
+ "username": "Avabot",
+ "avatar_url": "https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/avatechai.png",
+ "content": "[API Call] New register!",
+ }
+ headers = {
+ "Content-Type": "application/json",
+ }
+ response = requests.post(webhook_url, headers=headers, data=json.dumps(data))
+
+ return modelId
+
+
+def randomSeed(num_digits=15):
+ range_start = 10 ** (num_digits - 1)
+ range_end = (10**num_digits) - 1
+ return random.randint(range_start, range_end)
+
+
+def load_workflow(workflow_name):
+ with open(
+ os.path.join(
+ os.path.dirname(__file__),
+ f"workflow_templates/api/{workflow_name}.json",
+ )
+ ) as f:
+ return "\n".join(f.readlines())
+
+
+default_workflow = load_workflow("avatar_generation_mask_api_v11(FaceToon)")
+
+
+@server.PromptServer.instance.routes.post("/avatar_generation")
+async def post_prompt_block(request):
+ prompt_server = server.PromptServer.instance
+ post = await request.post()
+ uploaded_workflow = post.get("workflow")
+ workflow_name = post.get("workflow_name")
+ if uploaded_workflow is not None:
+ workflow = uploaded_workflow
+ elif workflow_name is not None:
+ workflow = load_workflow(workflow_name)
+ else:
+ workflow = default_workflow
+
+ ref_image = post.get("ref_image")
+ base_image = post.get("base_image")
+ if ref_image is not None:
+ image_path = save_image(ref_image)
+ image_name, image_ext = os.path.splitext(image_path)
+ workflow = workflow.replace("reference_image_avatech", image_path)
+ elif base_image is not None:
+ image_path = save_image(base_image)
+ image_name, image_ext = os.path.splitext(image_path)
+ workflow = workflow.replace("base_image", image_path)
+ workflow = workflow.replace("reference_image_avatech", image_path) # TMP
+
+ for key, value in post.items():
+ if key.startswith("mask_"):
+ mask_name = image_name + "_" + key.replace("mask_", "") + image_ext
+ mask_path = save_image(value, save_name=mask_name)
+ workflow = workflow.replace(key, mask_path)
+
+ workflow = workflow.replace("embedding_id_avatech", image_path)
+ workflow = workflow.replace("SEED", str(randomSeed()))
+ api_prompt = json.loads(workflow)
+
+ # skip generation part if base_image is provided
+ if base_image is not None:
+ for value in api_prompt.values():
+ if (
+ value["class_type"] == "LoadImageFromRequest"
+ and value["inputs"]["name"] == image_path
+ ):
+ del value["inputs"]["image"]
+ elif (
+ value["class_type"] == "PreviewImage"
+ or value["class_type"] == "SaveImage"
+ ):
+ value["inputs"] = {}
+
+ res = post_prompt({"prompt": api_prompt})
+ prompt_id = json.loads(res.text)["prompt_id"]
+ while True:
+ history = prompt_server.prompt_queue.get_history(prompt_id=prompt_id)
+ if history:
+ # file = get_avatar_file(history[prompt_id]["outputs"])
+ # return web.Response(body=file)
+
+ modelId = upload_avatar_file(history[prompt_id]["outputs"])
+ print("model id", modelId)
+ return web.json_response({"id": modelId}, status=200)
+ time.sleep(0.5)
+
+
+# @server.PromptServer.instance.routes.get("/get_default_workflow")
+# async def get_default_workflow(request):
+# # json_link = "https://cdn.discordapp.com/attachments/1119102674437156984/1172255632586448987/workflow_boy_2_1.json?ex=655fa722&is=654d3222&hm=463fa6a3c6ea60f7471196ff45382c729d3b856e86282f905d37a0398711860e&" # YP workflow
+# # json_link = "https://cdn.discordapp.com/attachments/729003657483518063/1172504658812608572/workflow_15.json?ex=65608f0e&is=654e1a0e&hm=f707d887b9294c1e9b26e54856b1e516d1725a1b25d044b46229cea6e5c804a1&" # Benny workflow
+# # json_link = 'https://cdn.discordapp.com/attachments/1110859802701221898/1173536418337914970/newstyle.json?ex=65644ff5&is=6551daf5&hm=f129838fae10197351bd27c69c7ff5eb4edf2c7d6ed74e6db8b55ddaa3c77dee&' # Deepwoo workflow
+# json_link = 'https://cdn.discordapp.com/attachments/729003657483518063/1174045115757633596/girl1114.json?ex=656629b8&is=6553b4b8&hm=df3d7798b887e2b3b6b06ea438f1bc4ba041dd0f9daf54ea48101845ec7f4243&'
+# response = requests.get(json_link)
+# response.raise_for_status()
+# return web.json_response(response.json())
+
+
+@server.PromptServer.instance.routes.get("/get_workflow")
+async def get_workflow(request):
+ name = request.rel_url.query.get("name", "default")
+ # if name == "default":
+ # json_link = 'https://cdn.discordapp.com/attachments/729003657483518063/1174045115757633596/girl1114.json?ex=656629b8&is=6553b4b8&hm=df3d7798b887e2b3b6b06ea438f1bc4ba041dd0f9daf54ea48101845ec7f4243&'
+ # response = requests.get(json_link)
+ # response.raise_for_status()
+ # workflow = response.json()
+ # else:
+ if name == "default":
+ name = "Auto_segment_workflow"
+
+ workflows_path = os.path.join(os.path.dirname(__file__), "workflow_templates")
+ workflow = json.load(open(f"{workflows_path}/{name}.json"))
+ return web.json_response(workflow)
+
@server.PromptServer.instance.routes.post("/segments")
async def post_segments(request):
post = await request.json()
name = post.get("name")
segments = post.get("segments")
- os.makedirs(os.path.join(folder_paths.base_path, f"output/{name}"), exist_ok=True)
+ output_dir = os.path.join(folder_paths.base_path, f"output/segments_{name}")
+ os.makedirs(output_dir, exist_ok=True)
for key, value in segments.items():
- filename = os.path.join(folder_paths.base_path, f"output/{name}/{key}.png")
+ filename = os.path.join(output_dir, f"{key}.png")
with open(filename, "wb") as f:
f.write(base64.b64decode(value.split(",")[1]))
- return web.json_response({})
-
-@server.PromptServer.instance.routes.post("/segments_order")
-async def post_segments(request):
- post = await request.json()
- name = post.get("name")
- order = post.get("order")
- output_dir = os.path.join(folder_paths.base_path, f"output/{name}")
- os.makedirs(output_dir, exist_ok=True)
- with open(os.path.join(output_dir, "order.json") , "w") as f:
+ order = list(segments.keys())
+ with open(os.path.join(output_dir, "order.json"), "w") as f:
json.dump(order, f)
return web.json_response({})
+
+
+# @server.PromptServer.instance.routes.post("/segments_order")
+# async def post_segments(request):
+# post = await request.json()
+# name = post.get("name")
+# order = post.get("order")
+# output_dir = os.path.join(folder_paths.base_path, f"output/{name}")
+# os.makedirs(output_dir, exist_ok=True)
+# with open(os.path.join(output_dir, "order.json") , "w") as f:
+# json.dump(order, f)
+# return web.json_response({})
+
+
+@server.PromptServer.instance.routes.get("/get_webhook")
+async def get_webhook(request):
+ url = os.getenv("DISCORD_WEBHOOK_URL")
+ return web.json_response(url)
+
+
+import uuid
+
+
+@server.PromptServer.instance.routes.post("/create_avatar_from_image")
+async def post_input_file(request):
+ post = await request.read()
+
+ # Doesn't seems working when file isnt png / or nothing is uploaded
+ if not post:
+ raise web.HTTPBadRequest(reason="No image data received")
+
+ try:
+ queue_id = uuid.uuid4()
+
+ output_dir = os.path.join(
+ folder_paths.base_path, "input", "create_avatar_endpoint"
+ )
+ os.makedirs(output_dir, exist_ok=True)
+
+ filename = os.path.join(output_dir, str(queue_id) + ".png")
+ with open(filename, "wb") as f:
+ f.write(post)
+
+ return web.json_response(
+ {
+ "redirect_url": "https://ai-assistant.avatech.ai?queue-id="
+ + str(queue_id)
+ }
+ )
+ except Exception as e:
+ print(e)
+ return web.json_response({"error": e})
diff --git a/sam/load_image_from_request.py b/sam/load_image_from_request.py
new file mode 100644
index 0000000..f18107a
--- /dev/null
+++ b/sam/load_image_from_request.py
@@ -0,0 +1,43 @@
+import folder_paths
+from PIL import Image, ImageOps
+import numpy as np
+import torch
+
+class LoadImageFromRequest:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "name": (
+ "STRING",
+ {"multiline": False, "default": "face.png"},
+ ),
+ },
+ "optional": {
+ "image": ("IMAGE",),
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE",)
+ RETURN_NAMES = ("image",)
+
+ FUNCTION = "run"
+
+ CATEGORY = "image"
+
+ def run(self, name, image=None):
+ try:
+ image_path = folder_paths.get_annotated_filepath(name)
+ image = Image.open(image_path)
+ image = ImageOps.exif_transpose(image)
+ # image = image.convert("RGB")
+ image = np.array(image).astype(np.float32) / 255.0
+ image = torch.from_numpy(image)[None,]
+ return [image]
+ except:
+ return [image]
+
+
+NODE_CLASS_MAPPINGS = {"LoadImageFromRequest": LoadImageFromRequest}
+
+NODE_DISPLAY_NAME_MAPPINGS = {"LoadImageFromRequest": "Load Image From Request"}
diff --git a/sam/sam_multilayer.py b/sam/sam_multilayer.py
index 35a80a6..3a7bfbd 100644
--- a/sam/sam_multilayer.py
+++ b/sam/sam_multilayer.py
@@ -7,9 +7,96 @@ import json
from segment_anything import sam_model_registry, SamPredictor
from einops import rearrange, repeat
from PIL import Image
+import mediapipe as mp
+from math import sqrt
+BaseOptions = mp.tasks.BaseOptions
+FaceLandmarker = mp.tasks.vision.FaceLandmarker
+FaceLandmarkerOptions = mp.tasks.vision.FaceLandmarkerOptions
+PoseLandmarker = mp.tasks.vision.PoseLandmarker
+PoseLandmarkerOptions = mp.tasks.vision.PoseLandmarkerOptions
+VisionRunningMode = mp.tasks.vision.RunningMode
global_predictor = None
+face_landmarker = None
+pose_landmarker = None
+
+# For auto-segmentation
+layerMapping = {
+ "L_eye": {
+ "useMiddle": False,
+ "positiveOffsetX": 0,
+ "positiveOffsetY": 0,
+ "negativeOffsetX": 0,
+ "negativeOffsetY": 0,
+ "positiveScale": 0,
+ "negativeScale": 0.5,
+ "indices": mp.solutions.face_mesh.FACEMESH_LEFT_EYE,
+ },
+ "R_eye": {
+ "useMiddle": False,
+ "positiveOffsetX": 0,
+ "positiveOffsetY": 0,
+ "negativeOffsetX": 0,
+ "negativeOffsetY": 0,
+ "positiveScale": 0,
+ "negativeScale": 0.5,
+ "indices": mp.solutions.face_mesh.FACEMESH_RIGHT_EYE,
+ },
+ "L_iris": {
+ "useMiddle": False,
+ "positiveOffsetX": 0,
+ "positiveOffsetY": 0,
+ "negativeOffsetX": 0,
+ "negativeOffsetY": 0,
+ "positiveScale": -0.2,
+ "negativeScale": 0.5,
+ "indices": mp.solutions.face_mesh.FACEMESH_LEFT_IRIS,
+ },
+ "R_iris": {
+ "useMiddle": False,
+ "positiveOffsetX": 0,
+ "positiveOffsetY": 0,
+ "negativeOffsetX": 0,
+ "negativeOffsetY": 0,
+ "positiveScale": -0.2,
+ "negativeScale": 0.5,
+ "indices": mp.solutions.face_mesh.FACEMESH_RIGHT_IRIS,
+ },
+ "face": {
+ "useMiddle": False,
+ "positiveOffsetX": 0,
+ "positiveOffsetY": 40,
+ "negativeOffsetX": 0,
+ "negativeOffsetY": 60,
+ "positiveScale": 0.2,
+ "negativeScale": 0.6,
+ "indices": mp.solutions.face_mesh.FACEMESH_FACE_OVAL,
+ },
+ "mouth": {
+ "useMiddle": False,
+ "positiveOffsetX": 0,
+ "positiveOffsetY": 0,
+ "negativeOffsetX": 0,
+ "negativeOffsetY": 0,
+ "positiveScale": -0.3,
+ "negativeScale": 0.3,
+ # https://stackoverflow.com/questions/66649492/how-to-get-specific-landmark-of-face-like-lips-or-eyes-using-tensorflow-js-face
+ "indices": [[x, x] for x in [61, 37, 270, 91, 314]],
+ },
+ "mouth_in": {
+ "useMiddle": False,
+ "positiveOffsetX": 0,
+ "positiveOffsetY": 0,
+ "negativeOffsetX": 0,
+ "negativeOffsetY": 0,
+ "positiveScale": -0.5,
+ "negativeScale": 0.5,
+ # https://stackoverflow.com/questions/66649492/how-to-get-specific-landmark-of-face-like-lips-or-eyes-using-tensorflow-js-face
+ "indices": [[x, x] for x in [310, 88]],
+ },
+}
+
class SAMMultiLayer:
def __init__(self):
@@ -18,12 +105,6 @@ class SAMMultiLayer:
@classmethod
def INPUT_TYPES(s):
- input_dir = folder_paths.get_input_directory()
- files = [
- f
- for f in os.listdir(input_dir)
- if os.path.isfile(os.path.join(input_dir, f))
- ]
return {
"required": {
"image": ("IMAGE",),
@@ -32,7 +113,6 @@ class SAMMultiLayer:
"STRING",
{"multiline": False, "default": "embedding"},
),
- # "image": (sorted(files), ),
"image_prompts_json": ("STRING", {"multiline": False, "default": "[]"}),
},
}
@@ -42,77 +122,249 @@ class SAMMultiLayer:
RETURN_TYPES = ("SAM_PROMPT",)
FUNCTION = "load_image"
+ def load_models(self, ckpt, model_type):
+ global global_predictor, face_landmarker, pose_landmarker
+
+ ckpt = folder_paths.get_full_path("sams", ckpt)
+ sam = sam_model_registry[model_type](checkpoint=ckpt) # .to("cuda")
+ global_predictor = SamPredictor(sam)
+
+ face_landmarker_model_path = os.path.join(
+ os.path.dirname(__file__), "../mediapipe_models/face_landmarker.task"
+ )
+ face_landmarker_options = FaceLandmarkerOptions(
+ base_options=BaseOptions(model_asset_path=face_landmarker_model_path),
+ running_mode=VisionRunningMode.IMAGE,
+ )
+ face_landmarker = FaceLandmarker.create_from_options(face_landmarker_options)
+
+ pose_landmarker_model_path = os.path.join(
+ os.path.dirname(__file__), "../mediapipe_models/pose_landmarker_full.task"
+ )
+ pose_landmarker_options = PoseLandmarkerOptions(
+ base_options=BaseOptions(model_asset_path=pose_landmarker_model_path),
+ running_mode=VisionRunningMode.IMAGE,
+ )
+ pose_landmarker = PoseLandmarker.create_from_options(pose_landmarker_options)
+ return global_predictor, face_landmarker, pose_landmarker
+
+ def auto_segment(self, image, face_landmarks, pose_landmarks):
+ H, W, C = image.shape
+ imagePromptsMulti = {}
+ boxesMulti = {}
+
+ for key, value in layerMapping.items():
+ positivePoints = []
+ middlePoints = []
+ negativePoints = []
+
+ for index in value["indices"]:
+ start, end = index
+ startPoint = face_landmarks[start]
+
+ startX = startPoint.x * W
+ startY = startPoint.y * H
+
+ if len(middlePoints) == 0:
+ middlePoints.append({"x": startX, "y": startY, "label": 1})
+ else:
+ middlePoints[0]["x"] += startX
+ middlePoints[0]["y"] += startY
+
+ positivePoints.append({"x": startX, "y": startY, "label": 1})
+
+ len_indices = len(value["indices"])
+ middlePoints[0]["x"] /= len_indices
+ middlePoints[0]["y"] /= len_indices
+
+ if value["useMiddle"]:
+ imagePromptsMulti[key] = middlePoints
+ else:
+ for i, index in enumerate(value["indices"]):
+ start, end = index
+ startPoint = face_landmarks[start]
+
+ startX = startPoint.x * W
+ startY = startPoint.y * H
+
+ middlePoint = middlePoints[0]
+ directionVector = {
+ "x": middlePoint["x"] - startX,
+ "y": middlePoint["y"] - startY,
+ }
+ directionVectorLength = sqrt(
+ directionVector["x"] * directionVector["x"]
+ + directionVector["y"] * directionVector["y"]
+ )
+
+ if value["negativeScale"] != 0:
+ negativePointDistance = (
+ value["negativeScale"] * directionVectorLength
+ )
+ negativePoint = {
+ "x": startX
+ - (negativePointDistance * directionVector["x"])
+ / directionVectorLength
+ - value["negativeOffsetX"],
+ "y": startY
+ - (negativePointDistance * directionVector["y"])
+ / directionVectorLength
+ - value["negativeOffsetY"],
+ "label": 0,
+ }
+ negativePoints.append(negativePoint)
+
+ positivePointDistance = (
+ value["positiveScale"] * directionVectorLength
+ )
+ positivePoints[i] = {
+ "x": positivePoints[i]["x"]
+ - (positivePointDistance * directionVector["x"])
+ / directionVectorLength
+ - value["positiveOffsetX"],
+ "y": positivePoints[i]["y"]
+ - (positivePointDistance * directionVector["y"])
+ / directionVectorLength
+ - value["positiveOffsetY"],
+ "label": 1,
+ }
+
+ imagePromptsMulti[key] = positivePoints + negativePoints
+
+ points = negativePoints if len(negativePoints) > 0 else positivePoints
+ box = np.array(
+ [
+ min(x["x"] for x in points),
+ min(x["y"] for x in points),
+ max(x["x"] for x in points),
+ max(x["y"] for x in points),
+ ]
+ )
+ boxesMulti[key] = box
+
+ if pose_landmarks is not None:
+ positiveBreathX = (
+ (pose_landmarks[11].x + pose_landmarks[12].x) / 2
+ ) * W
+ positiveBreathY = (
+ (pose_landmarks[11].y + pose_landmarks[12].y) / 2
+ ) * H
+ negativeBreathX1 = pose_landmarks[0].x * W
+ negativeBreathY1 = pose_landmarks[0].y * H
+ negativeBreathX2 = pose_landmarks[9].x * W
+ negativeBreathY2 = pose_landmarks[9].y * H
+ negativeBreathX3 = pose_landmarks[10].x * W
+ negativeBreathY3 = pose_landmarks[10].y * H
+ imagePromptsMulti["breath"] = [
+ {"x": positiveBreathX, "y": positiveBreathY, "label": 1},
+ {"x": negativeBreathX1, "y": negativeBreathY1, "label": 0},
+ {"x": negativeBreathX2, "y": negativeBreathY2, "label": 0},
+ {"x": negativeBreathX3, "y": negativeBreathY3, "label": 0},
+ ]
+
+ return imagePromptsMulti, boxesMulti
+
+ def detect_face(self, np_image):
+ global face_landmarker, pose_landmarker
+ mp_image = mp.Image(
+ image_format=mp.ImageFormat.SRGB, data=(np_image * 255).astype(np.uint8)
+ )
+ face_landmarks = face_landmarker.detect(mp_image).face_landmarks
+ face_landmarks = face_landmarks[0] if len(face_landmarks) > 0 else None
+ pose_landmarks = pose_landmarker.detect(mp_image).pose_landmarks
+ pose_landmarks = pose_landmarks[0] if len(pose_landmarks) > 0 else None
+ imagePromptsMulti, boxesMulti = self.auto_segment(
+ np_image, face_landmarks, pose_landmarks
+ )
+
+ return imagePromptsMulti, boxesMulti
+
def load_image(self, image, ckpt, embedding_id, image_prompts_json):
- # global global_predictor
- # model_type = re.findall(r'vit_[lbh]', ckpt)[0]
+ image_prompts = json.loads(image_prompts_json.replace("'", '"'))
- # if global_predictor is None:
- # ckpt = folder_paths.get_full_path("sams", ckpt)
- # sam = sam_model_registry[model_type](checkpoint=ckpt)
- # predictor = SamPredictor(sam)
- # global_predictor = predictor
-
- # predictor = global_predictor
-
- # emb_filename = f"{self.output_dir}/{embedding_id}_{model_type}.npy"
- # if not os.path.exists(emb_filename):
- # image_np = (image[0].numpy() * 255).astype(np.uint8)
- # predictor.set_image(image_np)
- # emb = predictor.get_image_embedding().cpu().numpy()
- # np.save(emb_filename, emb)
+ order_file = f"{self.output_dir}/segments_{embedding_id}/order.json"
+ if os.path.exists(order_file):
+ # Frontend uploads segments images to backend => backend reads all segments images and passes them to next nodes
+ with open(order_file) as f:
+ order = json.load(f)
- # with open(f"{self.output_dir}/{embedding_id}_{model_type}.json", "w") as f:
- # data = {
- # "input_size": predictor.input_size,
- # "original_size": predictor.original_size,
- # }
- # json.dump(data, f)
- # else:
- # emb = np.load(emb_filename)
+ result = [image_prompts]
- # with open(f"{self.output_dir}/{embedding_id}_{model_type}.json") as f:
- # data = json.load(f)
- # predictor.input_size = data["input_size"]
- # predictor.features = torch.from_numpy(emb)
- # predictor.is_image_set = True
- # predictor.original_size = data["original_size"]
+ for segment in order:
+ image = Image.open(
+ f"{self.output_dir}/segments_{embedding_id}/{segment}.png"
+ )
+ image = np.array(image).astype(np.float32) / 255.0
+ image = torch.from_numpy(image)[None,]
+ result.append(image)
- # image_prompts = json.loads(image_prompts_json)
+ return result
+ else:
+ # Frontend uploads clicks coordinates to backend => backend runs SAM and passes the segments to next nodes
+ model_type = re.findall(r"vit_[lbh]", ckpt)[0]
- # result = [image_prompts]
+ global global_predictor
+ if global_predictor is None:
+ global_predictor, _, _ = self.load_models(ckpt, model_type)
- # if isinstance(image_prompts, list):
- # pass
- # elif all(isinstance(item, list) for item in image_prompts.values()):
- # for item in image_prompts.values():
- # if (len(item) == 0):
- # h, w, c = image[0].shape
- # result.append(torch.zeros(1, h, w, c))
- # continue
- # point_coords = np.array([[p['x'], p['y']] for p in item])
- # point_labels = np.array([p['label'] for p in item])
+ if image.shape[3] == 4:
+ image = image[:, :, :, :3]
- # masks, _, _ = predictor.predict(
- # point_coords=point_coords,
- # point_labels=point_labels,
- # )
- # masks = torch.from_numpy(masks)
- # masks = rearrange(masks[0], 'h w -> 1 h w')
- # out_image = repeat(masks, '1 h w -> 1 h w c', c=3) * image
- # result.append(out_image)
-
- image_prompts = json.loads(image_prompts_json)
- result = [image_prompts]
+ emb_filename = f"{self.output_dir}/{embedding_id}_{model_type}.npy"
+ if not os.path.exists(emb_filename):
+ image_np = (image[0].numpy() * 255).astype(np.uint8)
+ global_predictor.set_image(image_np)
+ emb = global_predictor.get_image_embedding().cpu().numpy()
+ np.save(emb_filename, emb)
- with open(f"{self.output_dir}/{embedding_id}/order.json") as f:
- order = json.load(f)
+ with open(
+ f"{self.output_dir}/{embedding_id}_{model_type}.json", "w"
+ ) as f:
+ data = {
+ "input_size": global_predictor.input_size,
+ "original_size": global_predictor.original_size,
+ }
+ json.dump(data, f)
+ else:
+ emb = np.load(emb_filename)
- for segment in order:
- image = Image.open(f"{self.output_dir}/{embedding_id}/{segment}.png")
- image = np.array(image).astype(np.float32) / 255.0
- image = torch.from_numpy(image)[None,]
- result.append(image)
- return result
+ with open(f"{self.output_dir}/{embedding_id}_{model_type}.json") as f:
+ data = json.load(f)
+ global_predictor.input_size = data["input_size"]
+ global_predictor.features = torch.from_numpy(emb)
+ global_predictor.is_image_set = True
+ global_predictor.original_size = data["original_size"]
+
+ imagePromptsMulti, boxesMulti = self.detect_face(image[0].numpy())
+
+ image_prompts = json.loads(image_prompts_json.replace("'", '"'))
+ result = [image_prompts]
+
+ if isinstance(image_prompts, list):
+ pass
+ elif all(isinstance(item, list) for item in image_prompts.values()):
+ for key, item in image_prompts.items():
+ if len(item) == 0:
+ h, w, c = image[0].shape
+ result.append(torch.zeros(1, h, w, c))
+ continue
+
+ points = (
+ imagePromptsMulti[key] if key in imagePromptsMulti else item
+ )
+ point_coords = np.array([[p["x"], p["y"]] for p in points])
+ point_labels = np.array([p["label"] for p in points])
+
+ masks, _, _ = global_predictor.predict(
+ point_coords=point_coords,
+ point_labels=point_labels,
+ box=boxesMulti[key] if key in boxesMulti else None,
+ )
+ masks = torch.from_numpy(masks)
+ masks = rearrange(masks[0], "h w -> 1 h w")
+ out_image = repeat(masks, "1 h w -> 1 h w c", c=3) * image
+ result.append(out_image)
+ return result
NODE_CLASS_MAPPINGS = {"SAM MultiLayer": SAMMultiLayer}
diff --git a/tailwind.config.js b/tailwind.config.js
index 8214ba5..d183623 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -3,6 +3,9 @@ module.exports = {
// content: ['./js/**/*.{html,js}'],
content: ['./js/**/*.{html,js}'],
theme: {
+ fontFamily: {
+ 'gabarito': ['Gabarito'],
+ },
extend: {},
},
daisyui: {