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: {