26 Commits
Author SHA1 Message Date
EdwinWong acc0a8d1cc fix: onnx runtime version 2024-02-06 16:56:24 +08:00
EdwinWong 82e3f4dbec fix: preview display 2024-01-24 11:53:03 +08:00
EdwinWong 10d93e0823 fix: notification 2024-01-19 19:50:27 +08:00
EdwinWong 2ded553837 fix: add output type in main output 2024-01-19 18:06:14 +08:00
EdwinWong 00a7d5a26c fix: nanoid 2024-01-19 18:05:17 +08:00
Radionic 980408b363 fix: route 2024-01-16 11:35:38 +08:00
Radionic eaaab6e346 feat: data generation 2024-01-11 15:26:16 +08:00
Radionic eccc4bca31 feat: render blender image 2024-01-09 12:56:34 +08:00
EdwinWong 8ed66b3260 fix: emb id 2024-01-04 17:59:34 +08:00
Radionic 7d0cfb6e5b fix: onnx model loading 2024-01-03 12:14:48 +08:00
Radionic a9c3075b70 feat: update sam node and combine points node 2024-01-03 12:14:35 +08:00
Radionic 0f29e2c27d fix: duplicated input 2024-01-02 11:15:41 +08:00
Radionic b3ee46fdf2 feat: extract boundary points and combine points node 2023-12-29 18:13:32 +08:00
Radionic b3e21ac98e feat: upload avatar avatar_main_output node 2023-12-29 15:38:28 +08:00
Radionic 9aaa894a2b feat: upload avatar in output node 2023-12-29 15:38:28 +08:00
Radionic 685e225d85 fix: set object property 2023-12-29 15:38:28 +08:00
BennyKok dfd130c595 chore: also return file name as images [] so comfy deploy can catch and upload the file
# Conflicts:
#	blender/avatar_main_output.py
2023-12-29 15:38:26 +08:00
Radionic d81449fc12 fix: mask_mouth_in mask not working 2023-12-29 15:37:57 +08:00
EdwinWong a538f7d379 fix: bpy requirement 2023-12-29 15:37:57 +08:00
BennyKok c1f0138fc7 fix: output name 2023-12-21 20:01:22 +08:00
BennyKok bdb9a99c90 fix: change images to files 2023-12-21 00:07:56 +08:00
BennyKok f8396e9a1b chore: also return file name as images [] so comfy deploy can catch and upload the file 2023-12-20 23:24:42 +08:00
EdwinWong 08b16d4efb refactor: back to top right corner 2023-12-20 19:04:37 +08:00
EdwinWong c1ba8164fc Merge branch 'app' into dev
# Conflicts:
#	js/LayerEditor.js
#	js/index.js
#	routes.py
#	sam/sam_multilayer.py
2023-12-20 18:28:47 +08:00
Radionic 587cac06af fix: segment order json 2023-11-10 17:59:13 +08:00
Radionic 0659bfb2e7 fix: inconsistent segments 2023-11-10 17:32:21 +08:00
20 changed files with 987 additions and 228 deletions
+26 -4
View File
@@ -1,6 +1,6 @@
import platform
import blender_node
from mesh_utils import assign_texture, open_in_blender as open_blender, export_gltf
from mesh_utils import upload_avatar_file, open_in_blender as open_blender, export_gltf
import folder_paths
global_blender_path = ''
@@ -47,14 +47,16 @@ class AvatarMainOutput(blender_node.ObjectOps):
}),
"model_type": (["AVA","GLB", "GLTF_EMBEDDED"],),
"write_mode": (["Overwrite", "Increment"],),
"upload_to_cloud": ("BOOLEAN", {
"default": False
}),
"SHAPE_FLOW": ("SHAPE_FLOW",),
}
OUTPUT_NODE = True
RETURN_TYPES = ()
def blender_process(self, bpy, BPY_OBJ=None, BPY_OBJS=None, open_in_blender=False, auto_save=False, blender_path_override='', filename='', model_type='', write_mode='', SHAPE_FLOW=''):
def blender_process(self, bpy, BPY_OBJ=None, BPY_OBJS=None, open_in_blender=False, auto_save=False, blender_path_override='', filename='', model_type='', write_mode='', upload_to_cloud=False, SHAPE_FLOW=''):
if open_in_blender:
p = blender_path_override if blender_path_override else global_blender_path
output_file = self.output_dir + '/tmp.blend'
@@ -72,4 +74,24 @@ class AvatarMainOutput(blender_node.ObjectOps):
import global_bpy
global_bpy.set_should_reset_scene(True)
return {"ui": {"gltfFilename": {filepath.replace(f"{self.output_dir}/", "")}, "SHAPE_FLOW": {SHAPE_FLOW}, "auto_save": {'true' if auto_save else 'false'},}}
outputs = {
"gltfFilename": [filepath.replace(f"{self.output_dir}/", "")],
"files": [{
"filename": filepath.replace(f"{self.output_dir}/", ""),
"content_type": "model/gltf+json",
"type": "output"
},],
"SHAPE_FLOW": {SHAPE_FLOW},
"auto_save": {'true' if auto_save else 'false'},
}
if upload_to_cloud:
avatarId = upload_avatar_file(outputs)
return {
"ui": {
**outputs,
"avatarId": [avatarId],
},
}
return {
"ui": outputs
}
+2 -1
View File
@@ -260,7 +260,8 @@ def create_primitive_shape_class(cls, path, name=None, name_prefix=''):
def assign_and_return(BPY_OBJ, name, value):
BPY_OBJ[name] = value
setattr(BPY_OBJ, name, value)
# BPY_OBJ[name] = value
# print(BPY_OBJ,name, BPY_OBJ[name])
return None
+39 -1
View File
@@ -1,6 +1,9 @@
import atexit
import subprocess
import os
import folder_paths
import requests
import json
def genreate_mesh_from_texture(bpy, image):
import torch
@@ -245,4 +248,39 @@ def export_gltf(output_dir, bpy_objects, filename, model_type, write_mode, metad
os.replace(filepath, new_filepath)
filepath = new_filepath
return filepath
return filepath
def get_avatar_file(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(output):
file = get_avatar_file(output)
response = requests.get("https://labs.avatech.ai/api/share?version=v2")
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
+1 -1
View File
@@ -41,7 +41,7 @@ class Object_CreateMeshLayer(blender_node.ObjectOps):
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.edge_face_add()
bpy.ops.transform.resize(value=(scale_x, scale_y, 1))
bpy.ops.transform.resize(value=(float(scale_x), float(scale_y), 1))
bpy.context.object.vertex_groups.new(name=mesh_layer_name)
bpy.ops.object.vertex_group_assign()
+26
View File
@@ -0,0 +1,26 @@
import blender_node
class Mesh_SetShapeKeyValue(blender_node.ObjectOps):
CUSTOM_NAME = "Set Shape Key Value"
EXTRA_INPUT_TYPES = {
"shape_key_name": ("STRING", {
"multiline": False,
"default": "my_shape_key",
}),
"value": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "number"}),
}
def blender_process(self, bpy, BPY_OBJ, shape_key_name, value):
# Check if the object has shape keys
if BPY_OBJ.data.shape_keys:
# Check if the specified shape key exists
if shape_key_name in BPY_OBJ.data.shape_keys.key_blocks:
BPY_OBJ.data.shape_keys.key_blocks[shape_key_name].value = float(value)
else:
print(f"The shape key {shape_key_name} does not exist on the object.")
else:
print("The object does not have any shape keys.")
return (BPY_OBJ,)
+132
View File
@@ -0,0 +1,132 @@
import blender_node
import math
import folder_paths
import torch
import numpy as np
import os
from PIL import Image, ImageOps
def get_incremented_filename(folder_path, base_filename):
# Initialize the counter and create the full initial path
counter = 0
output_path = f"{folder_path}/{base_filename}.png"
# Check if the file exists and increment the counter until the file does not exist
while os.path.exists(output_path):
counter += 1
output_path = f"{folder_path}/{base_filename}_{counter}.png"
return output_path
class BlenderRenderImage(blender_node.ObjectOps):
def __init__(self):
pass
EXTRA_INPUT_TYPES = {
}
# OUTPUT_NODE = True
RETURN_TYPES = ("BPY_OBJ", "IMAGE")
def add_light(self, bpy):
# Check if there is at least one light source in the scene
light_exists = any(ob for ob in bpy.data.objects if ob.type == 'LIGHT')
if not light_exists:
# Create a new Area light datablock for ambient light
light_data = bpy.data.lights.new(name='AmbientLight', type='AREA')
light_object = bpy.data.objects.new(name='AmbientLight', object_data=light_data)
bpy.context.collection.objects.link(light_object)
# Position the light in the scene
light_object.location = (0, 0, 10)
# Set light size for soft shadows and ambient effect
light_data.size = 10
light_data.energy = 1000
print("Added an ambient light source to the scene.")
def add_camera(self, bpy):
# Check if there is a camera in the scene
if bpy.context.scene.camera:
return bpy.context.scene.camera
# If not, create a new camera
cam_data = bpy.data.cameras.new(name='Camera')
cam = bpy.data.objects.new(name='Camera', object_data=cam_data)
bpy.context.collection.objects.link(cam)
# Set the new camera to the active camera
bpy.context.scene.camera = cam
# Position the camera to a default view
cam.location = (0, 0, 10)
return cam
def get_texture_size(self, obj):
# Get the first material slot
mat = obj.data.materials[0]
# Check if the material has a node tree
if mat.node_tree:
nodes = mat.node_tree.nodes
# Find an image texture node in the node tree
for node in nodes:
if node.type == 'TEX_IMAGE':
texture = node.image
if texture:
return texture.size
print("No image texture node found in the material's node tree.")
else:
print("Material has no node tree.")
def blender_process(self, bpy, BPY_OBJ=None):
cam = self.add_camera(bpy)
plane = BPY_OBJ
if plane:
tex_width, tex_height = self.get_texture_size(plane)
# Calculate the aspect ratio of the plane
aspect_ratio_plane = tex_width / tex_height
# Set the render resolution to match the plane's aspect ratio
# Choose an arbitrary resolution for the longer side of the plane
base_resolution = 512
if aspect_ratio_plane > 1:
# Plane is wider than it is tall
bpy.context.scene.render.resolution_x = base_resolution
bpy.context.scene.render.resolution_y = int(base_resolution / aspect_ratio_plane)
else:
# Plane is taller than it is wide
bpy.context.scene.render.resolution_x = int(base_resolution * aspect_ratio_plane)
bpy.context.scene.render.resolution_y = base_resolution
bpy.context.scene.render.resolution_percentage = 100
ortho_scale = max(plane.dimensions.x, plane.dimensions.y)
cam.data.type = 'ORTHO'
cam.data.ortho_scale = ortho_scale
self.add_light(bpy)
# Update the scene to reflect changes
bpy.context.view_layer.update()
# Set render engine (e.g., 'BLENDER_EEVEE', 'CYCLES', 'BLENDER_WORKBENCH')
bpy.context.scene.render.engine = "BLENDER_EEVEE"
# Specify the render output path
output_path = get_incremented_filename(folder_paths.get_output_directory(), "render")
bpy.context.scene.render.filepath = output_path
# Render the image
bpy.ops.render.render(write_still=True)
# Load the image
i = Image.open(output_path)
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
# print(image.shape)
return (BPY_OBJ, image)
+4 -47
View File
@@ -173,10 +173,6 @@ async function prepareImageFromUrlRedirect(stage) {
export function AvatarPreview() {
console.log("getting workflow json now");
loadJSONWorkflow("default").then(() => {
console.log("done loading");
jsonWorkflowLoading.val = false;
});
const loading = van.state(false);
const shareLoading = van.state("share"); // share, loading, shared
@@ -465,7 +461,7 @@ export function AvatarPreview() {
name: "avatech-viewer-iframe",
allow: "cross-origin-isolated",
class: () =>
"w-full h-full min-w-[400px] min-h-[400px] z-[100] pointer-events-auto flex border-none overflow-hidden" +
"w-full h-full min-w-[350px] min-h-[350px] z-[100] pointer-events-auto flex border-none overflow-hidden bg-transparent" +
(showPreview.val ? "" : "hidden"),
// src: "https://labs.avatech.ai/viewer/default",
// src: "http://localhost:3000/viewer/default",
@@ -710,51 +706,12 @@ export function AvatarPreview() {
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"
(showPreview.val && !showEditor.val ? "" : "hidden ") +
"absolute w-[360px] h-[360px] rounded-xl overflow-hidden right-0 top-0 z-[99] pointer-events-auto flex border-none bg-transparent"
);
},
},
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()
)
);
}
}
)
renderIFrame(),
);
}
+146
View File
@@ -0,0 +1,146 @@
import { combinePointsNode, samPrompts } from "./state.js";
import { van } from "./van.js";
const { div, dialog, form, button, h3, input, span } = van.tags;
van.derive(() => {
if (
combinePointsNode.val != undefined &&
combinePointsNode.val.type === "Combine Points"
) {
const inputNames = combinePointsNode.val.inputs?.map((x) => x.name) || [];
const record = Object.keys(samPrompts.val);
const diff = inputNames.filter((x) => !record.includes(x));
const missingDiff = record.filter((x) => !inputNames.includes(x));
if (diff.length > 0) {
diff.forEach((x) => {
combinePointsNode.val.removeInput(
combinePointsNode.val.findInputSlot(x)
);
});
combinePointsNode.val.graph.change();
}
if (missingDiff.length > 0) {
missingDiff.forEach((x) => {
combinePointsNode.val.addInput(x, "POINTS");
});
combinePointsNode.val.graph.change();
}
}
});
export function CombinePointsDialog() {
const showAddLayer = van.state(false);
return div(
{
class: () =>
"absolute z-[100] top-0 left-0 flex justify-center w-full h-full ",
},
() =>
dialog(
{ id: "combine_points_dialog", class: "modal" },
div(
{ class: "modal-box text-base-content" },
form(
{
class: "gap-2 flex flex-col",
method: "dialog",
onsubmit: (e) => {
e.preventDefault();
combine_points_dialog.close();
},
},
button(
{
type: "button",
class: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2",
onclick: (e) => {
e.stopPropagation();
combine_points_dialog.close();
},
},
"✕"
),
h3({ class: "font-bold text-lg text-base-content" }, "Edit points"),
() =>
div(
{ class: "flex flex-col gap-2 mb-2" },
...Object.keys(samPrompts.val).map((key) => {
return span(key);
})
),
() =>
showAddLayer.val
? div(
{
class:
"flex flex-row justify-center items-center border rounded-md pr-2",
},
input({
type: "text",
placeholder: "Type here",
id: "layerName",
class:
"input input-ghost w-full focus:ring-0 focus:border-none focus:outline-none",
autofocus: true,
}),
button(
{
onclick: (e) => {
e.stopPropagation();
e.preventDefault();
showAddLayer.val = false;
},
},
span({
class: "iconify text-2xl",
"data-icon": "iconoir:cancel",
})
),
button(
{
onclick: (e) => {
e.stopPropagation();
e.preventDefault();
showAddLayer.val = false;
const inputText =
document.getElementById("layerName").value;
samPrompts.val = {
...samPrompts.val,
[inputText]: [],
};
},
},
span({
class: "iconify text-2xl",
"data-icon": "mdi:tick",
})
)
)
: button(
{
class: "btn btn-outline btn",
onclick: (e) => {
e.stopPropagation();
e.preventDefault();
showAddLayer.val = true;
},
},
"Add new layer"
),
button(
{
type: "submit",
class: "btn btn-sm btn-ghost place-self-end",
},
"Confirm"
)
)
)
)
);
}
+11 -9
View File
@@ -1,18 +1,20 @@
import { LayerEditor } from './LayerEditor.js';
import { ShapeFlowEditor } from './ShapeFlowEditor.js';
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';
import { LayerEditor } from "./LayerEditor.js";
import { ShapeFlowEditor } from "./ShapeFlowEditor.js";
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";
import { CombinePointsDialog } from "./CombinePointsDialog.js";
const { button, iframe, div, img } = van.tags;
export function Container() {
return div(
{
class: 'fixed left-0 top-0 w-full h-full z-[1000] pointer-events-none',
id: 'avatech-editor',
class: "fixed left-0 top-0 w-full h-full z-[1000] pointer-events-none",
id: "avatech-editor",
},
CombinePointsDialog(),
ShapeFlowEditor(),
LayerEditor(),
AvatarPreview(),
+29 -24
View File
@@ -6,6 +6,7 @@ import {
targetNode,
showImageEditor,
allImagePrompts,
samPrompts,
} from "./state.js";
import { van } from "./van.js";
const {
@@ -24,6 +25,33 @@ const {
span,
} = van.tags;
export const updateOutputs = () => {
const outputNames = targetNode.val.outputs.map((x) => x.name).slice(1);
const record = Object.keys(imagePromptsMulti.val);
const diff = outputNames.filter((x) => !record.includes(x));
const missingDiff = record.filter((x) => !outputNames.includes(x));
if (diff.length > 0) {
console.log("Cleaning up missing output slots", diff);
diff.forEach((x) => {
targetNode.val.removeOutput(targetNode.val.findOutputSlot(x));
});
targetNode.val.graph.change();
}
if (missingDiff.length > 0) {
console.log("Adding missing output slots", diff);
missingDiff.forEach((x) => {
targetNode.val.addOutput(
x,
targetNode.val.type === "SAM MultiLayer" ? "IMAGE" : "SAM_PROMPT"
);
});
targetNode.val.graph.change();
}
};
van.derive(() => {
if (
showImageEditor.val &&
@@ -31,30 +59,7 @@ van.derive(() => {
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);
const diff = outputNames.filter((x) => !record.includes(x));
const missingDiff = record.filter((x) => !outputNames.includes(x));
if (diff.length > 0) {
console.log("Cleaning up missing output slots", diff);
diff.forEach((x) => {
targetNode.val.removeOutput(targetNode.val.findOutputSlot(x));
});
targetNode.val.graph.change();
}
if (missingDiff.length > 0) {
console.log("Adding missing output slots", diff);
missingDiff.forEach((x) => {
targetNode.val.addOutput(
x,
targetNode.val.type === "SAM MultiLayer" ? "IMAGE" : "SAM_PROMPT"
);
});
targetNode.val.graph.change();
}
updateOutputs();
}
});
+46 -5
View File
@@ -5,6 +5,7 @@ import {
imageUrl,
imagePrompts,
targetNode,
combinePointsNode,
fileName,
embeddings,
imagePromptsMulti,
@@ -17,6 +18,7 @@ import {
previewModelId,
embeddingID,
enableAutoSegment,
samPrompts,
} from "./state.js";
import { van } from "./van.js";
import { app } from "./app.js";
@@ -33,6 +35,7 @@ import {
import { infoDialog } from "./dialog.js";
import { sharedAvatarLink } from "./AvatarPreview.js";
import { updateImagePrompts } from "./LayerEditor.js";
import { updateOutputs } from "./SideBar.js";
export const generatedImages = {};
@@ -357,7 +360,6 @@ function showMyImageEditor(node) {
drawSegment(getClicks());
updateImagePrompts();
});
targetNode.val = node;
})
.catch((err) => {
console.log(err);
@@ -385,6 +387,25 @@ const ext = {
showMyImageEditor(node);
btn.serialize = false;
});
targetNode.val = node;
node.onConnectInput = (node, slot, targetSlot) => {
if (targetSlot.name === "SAM_PROMPTS") {
imagePromptsMulti.val = samPrompts.val;
updateOutputs();
}
};
return {
widget: btn,
};
},
COMBINE_POINTS(node, inputName, inputData, app) {
const btn = node.addWidget("button", "Edit points", "", () => {
console.log("Edit points");
combine_points_dialog.showModal();
combinePointsNode.val = node;
});
return {
widget: btn,
};
@@ -675,6 +696,18 @@ const ext = {
nodeData.input.required.sam = ["SAM_PROMPTS"];
// nodeData.input.required.upload = ['IMAGEUPLOAD'];
// nodeData.input.required.prompts_points = ["IMAGEUPLOAD"];
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: "Open In Points Editor (Local)",
callback: () => {
showMyImageEditor(this);
},
});
});
break;
case "Combine Points":
nodeData.input.required.sam = ["COMBINE_POINTS"];
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: "Open In Points Editor (Local)",
@@ -746,11 +779,19 @@ function injectUIComponentToComfyuimenu() {
if (!filename.toLowerCase().endsWith(".json")) {
filename += ".json";
}
app.graphToPrompt().then(p=>{
console.log('fkfk');
app.graphToPrompt().then((p) => {
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"});
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;
+6 -4
View File
@@ -1,16 +1,18 @@
import "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js";
import "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.16.3/dist/ort.min.js";
import npyjs from "https://esm.sh/npyjs";
import { imageSize } from "./state.js";
import { modelData, onnxMaskToImage } from "./onnx_helper.js";
ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/";
ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.16.3/dist/";
export let model = null;
let modelType = null;
// Initialize the ONNX model
export const initModel = async (modelType) => {
export const initModel = async (type) => {
try {
if (!model) {
if (!model || modelType !== type) {
modelType = type;
model = await ort.InferenceSession.create(
`${location.protocol}//${location.host}/sam_model?type=${modelType}`
);
+9 -4
View File
@@ -19,13 +19,15 @@
*/
import { van } from "./van.js";
export const iframeSrc = van.state("https://editor.avatech.ai?comfyui=true");
export const showEditor = van.state(false);
// localStorage.getItem("showPreview") == 'true'
export const showPreview = van.state(true);
console.log(localStorage.getItem("showPreview"));
if (localStorage.getItem("showPreview") == null)
localStorage.setItem("showPreview", 'true')
export const showPreview = van.state(localStorage.getItem("showPreview") == '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"
"https://editor.avatech.ai/viewer?avatarId=default&debug=true&width=350&height=350&hideTrigger=true&voiceSelection=true&hideUI=true"
);
export const previewImg = van.state("");
export const previewImgLoading = van.state(false);
@@ -63,7 +65,6 @@ export const imagePrompts = van.state([]);
export const allImagePrompts = van.state([{}]);
/** @type {State<Record<string, Point[]>>} */
export const imagePromptsMulti = van.state({});
@@ -76,3 +77,7 @@ export const targetNode = van.state();
export const imageSize = van.state({ width: 0, height: 0, samScale: 0 });
export const embeddings = van.state();
export const embeddingID = van.state("Test");
/** @type {State<LGraphNode>} */
export const combinePointsNode = van.state();
export const samPrompts = van.state({});
+311 -72
View File
@@ -1,7 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Gabarito&display=swap');
/*
! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com
*/
/*
@@ -501,7 +501,7 @@ html{
:root{
color-scheme: light;
--in: 0.7206 0.191 231.6;
--su: 0.7441 0.213 164.75;
--su: 64.8% 0.150 160;
--wa: 0.8471 0.199 83.87;
--er: 0.7176 0.221 22.18;
--pc: 0.89824 0.06192 275.75;
@@ -742,7 +742,7 @@ html{
justify-content: center;
border-radius: var(--rounded-btn, 0.5rem);
border-color: transparent;
border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1));
border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity));
padding-left: 1rem;
padding-right: 1rem;
text-align: center;
@@ -762,7 +762,9 @@ html{
--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));
background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity));
--tw-bg-opacity: 1;
--tw-border-opacity: 1;
}
.btn-disabled,
@@ -870,8 +872,7 @@ html{
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)));
background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
}
.dropdown{
@@ -959,18 +960,23 @@ html{
.btn:hover{
--tw-border-opacity: 1;
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: 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
);
}
@supports (color: color-mix(in oklab, black, black)){
.btn:hover{
background-color: color-mix(
in oklab,
oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%,
black
);
border-color: color-mix(
in oklab,
oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%,
black
);
}
}
@supports not (color: oklch(0 0 0)){
@@ -995,6 +1001,99 @@ html{
}
}
.btn-outline:hover{
--tw-border-opacity: 1;
border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)));
}
.btn-outline.btn-primary:hover{
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-primary: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);
}
}
.btn-outline.btn-secondary:hover{
--tw-text-opacity: 1;
color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-secondary:hover{
background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
}
}
.btn-outline.btn-accent:hover{
--tw-text-opacity: 1;
color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-accent:hover{
background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
}
}
.btn-outline.btn-success:hover{
--tw-text-opacity: 1;
color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-success:hover{
background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
}
}
.btn-outline.btn-info:hover{
--tw-text-opacity: 1;
color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-info:hover{
background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
}
}
.btn-outline.btn-warning:hover{
--tw-text-opacity: 1;
color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-warning:hover{
background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
}
}
.btn-outline.btn-error:hover{
--tw-text-opacity: 1;
color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-error:hover{
background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
}
}
.btn-disabled:hover,
.btn[disabled]:hover,
.btn:disabled:hover{
@@ -1005,9 +1104,11 @@ html{
--tw-text-opacity: 0.2;
}
.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);
@supports (color: color-mix(in oklab, black, black)){
.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{
@@ -1018,8 +1119,6 @@ html{
: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;
--tw-text-opacity: 1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
outline: 2px solid transparent;
outline-offset: 2px;
}
@@ -1050,6 +1149,9 @@ html{
.input{
flex-shrink: 1;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
height: 3rem;
padding-left: 1rem;
padding-right: 1rem;
@@ -1152,10 +1254,8 @@ html{
.menu :where(li ul){
position: relative;
white-space: nowrap;
-webkit-margin-start: 1rem;
margin-inline-start: 1rem;
-webkit-padding-start: 0.5rem;
padding-inline-start: 0.5rem;
margin-inline-start: 1rem;
padding-inline-start: 0.5rem;
}
.menu :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)),
@@ -1336,6 +1436,43 @@ html{
}
}
@supports (color: color-mix(in oklab, black, black)){
.btn-outline.btn-primary.btn-active{
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);
}
.btn-outline.btn-secondary.btn-active{
background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
}
.btn-outline.btn-accent.btn-active{
background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
}
.btn-outline.btn-success.btn-active{
background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
}
.btn-outline.btn-info.btn-active{
background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
}
.btn-outline.btn-warning.btn-active{
background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
}
.btn-outline.btn-error.btn-active{
background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
}
}
.btn:focus-visible{
outline-style: solid;
outline-width: 2px;
@@ -1382,6 +1519,95 @@ html{
background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.btn-outline{
border-color: currentColor;
background-color: transparent;
--tw-text-opacity: 1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
--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);
}
.btn-outline.btn-active{
--tw-border-opacity: 1;
border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)));
}
.btn-outline.btn-primary{
--tw-text-opacity: 1;
color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
}
.btn-outline.btn-primary.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
.btn-outline.btn-secondary{
--tw-text-opacity: 1;
color: var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)));
}
.btn-outline.btn-secondary.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));
}
.btn-outline.btn-accent{
--tw-text-opacity: 1;
color: var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)));
}
.btn-outline.btn-accent.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)));
}
.btn-outline.btn-success{
--tw-text-opacity: 1;
color: var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)));
}
.btn-outline.btn-success.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));
}
.btn-outline.btn-info{
--tw-text-opacity: 1;
color: var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)));
}
.btn-outline.btn-info.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));
}
.btn-outline.btn-warning{
--tw-text-opacity: 1;
color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)));
}
.btn-outline.btn-warning.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));
}
.btn-outline.btn-error{
--tw-text-opacity: 1;
color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)));
}
.btn-outline.btn-error.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
}
.btn.btn-disabled,
.btn[disabled],
.btn:disabled{
@@ -1485,8 +1711,7 @@ details.collapse summary::-webkit-details-marker{
:where(.collapse > input[type="radio"]){
width: 100%;
padding: 1rem;
-webkit-padding-end: 3rem;
padding-inline-end: 3rem;
padding-inline-end: 3rem;
min-height: 3.75rem;
transition: background-color 0.2s ease-out;
}
@@ -1554,6 +1779,18 @@ details.collapse summary::-webkit-details-marker{
outline-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.input-ghost{
--tw-bg-opacity: 0.05;
}
.input-ghost:focus,
.input-ghost:focus-within{
--tw-bg-opacity: 1;
--tw-text-opacity: 1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
box-shadow: none;
}
.input-disabled,
.input:disabled,
.input[disabled]{
@@ -1577,11 +1814,14 @@ details.collapse summary::-webkit-details-marker{
--tw-placeholder-opacity: 0.2;
}
.input::-webkit-date-and-time-value{
text-align: inherit;
}
.join > :where(*:not(:first-child)){
margin-top: 0px;
margin-bottom: 0px;
-webkit-margin-start: -1px;
margin-inline-start: -1px;
margin-inline-start: -1px;
}
.link:focus{
@@ -1624,7 +1864,9 @@ details.collapse summary::-webkit-details-marker{
}
:where(.menu li:empty){
background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
opacity: 0.1;
margin: 0.5rem 1rem;
height: 1px;
}
@@ -1635,7 +1877,9 @@ details.collapse summary::-webkit-details-marker{
inset-inline-start: 0px;
top: 0.75rem;
width: 1px;
background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
opacity: 0.1;
content: "";
}
@@ -1760,7 +2004,7 @@ details.collapse summary::-webkit-details-marker{
.modal:not(dialog:not(.modal-open)),
.modal::backdrop{
background-color: rgba(0, 0, 0, 0.3);
background-color: #0006;
animation: modal-pop 0.2s ease-out;
}
@@ -2083,8 +2327,7 @@ details.collapse summary::-webkit-details-marker{
.join.join-horizontal > :where(*:not(:first-child)){
margin-top: 0px;
margin-bottom: 0px;
-webkit-margin-start: -1px;
margin-inline-start: -1px;
margin-inline-start: -1px;
}
.modal-top :where(.modal-box){
@@ -2174,10 +2417,6 @@ details.collapse summary::-webkit-details-marker{
left: 50%;
}
.left-28{
left: 7rem;
}
.left-4{
left: 1rem;
}
@@ -2230,6 +2469,10 @@ details.collapse summary::-webkit-details-marker{
z-index: 200;
}
.z-\[999\]{
z-index: 999;
}
.z-\[99\]{
z-index: 99;
}
@@ -2268,10 +2511,6 @@ details.collapse summary::-webkit-details-marker{
margin-top: 0.5rem;
}
.mt-4{
margin-top: 1rem;
}
.mt-8{
margin-top: 2rem;
}
@@ -2300,21 +2539,20 @@ details.collapse summary::-webkit-details-marker{
height: 24rem;
}
.h-\[394px\]{
height: 394px;
.h-\[360px\]{
height: 360px;
}
.h-fit{
height: -moz-fit-content;
height: fit-content;
.h-\[394px\]{
height: 394px;
}
.h-full{
height: 100%;
}
.min-h-\[400px\]{
min-height: 400px;
.min-h-\[350px\]{
min-height: 350px;
}
.w-20{
@@ -2341,6 +2579,10 @@ details.collapse summary::-webkit-details-marker{
width: 32rem;
}
.w-\[360px\]{
width: 360px;
}
.w-fit{
width: -moz-fit-content;
width: fit-content;
@@ -2350,8 +2592,8 @@ details.collapse summary::-webkit-details-marker{
width: 100%;
}
.min-w-\[400px\]{
min-width: 400px;
.min-w-\[350px\]{
min-width: 350px;
}
.max-w-\[100\%\]{
@@ -2404,10 +2646,6 @@ details.collapse summary::-webkit-details-marker{
justify-content: space-between;
}
.justify-around{
justify-content: space-around;
}
.gap-2{
gap: 0.5rem;
}
@@ -2424,14 +2662,6 @@ details.collapse summary::-webkit-details-marker{
overflow: hidden;
}
.overflow-y-auto{
overflow-y: auto;
}
.overflow-x-hidden{
overflow-x: hidden;
}
.rounded{
border-radius: 0.25rem;
}
@@ -2456,6 +2686,10 @@ details.collapse summary::-webkit-details-marker{
border-radius: 0.125rem;
}
.rounded-xl{
border-radius: 0.75rem;
}
.rounded-b-md{
border-bottom-right-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
@@ -2669,10 +2903,6 @@ details.collapse summary::-webkit-details-marker{
padding: 0.5rem;
}
.p-24{
padding: 6rem;
}
.p-4{
padding: 1rem;
}
@@ -2692,16 +2922,15 @@ details.collapse summary::-webkit-details-marker{
padding-right: 1rem;
}
.py-16{
padding-top: 4rem;
padding-bottom: 4rem;
}
.py-2{
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.pr-2{
padding-right: 0.5rem;
}
.text-start{
text-align: start;
}
@@ -3026,11 +3255,21 @@ img[src] {
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.focus\:border-none:focus{
border-style: none;
}
.focus\:outline-none:focus{
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus\:ring-0:focus{
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
@media (min-width: 640px){
.sm\:flex{
display: flex;
+1 -1
View File
@@ -3,7 +3,7 @@ numpy
opencv-python
opencv-contrib-python
einops
bpy
bpy==3.6.0
segment-anything
tqdm
python-dotenv
+54 -48
View File
@@ -2,6 +2,7 @@ from aiohttp import web
from segment_anything import sam_model_registry, SamPredictor
from PIL import Image, ImageOps
from dotenv import load_dotenv
from blender.mesh_utils import upload_avatar_file
import os
import requests
import folder_paths
@@ -125,7 +126,7 @@ async def post_sam_model(request):
}
json.dump(data, f)
else:
sam = sam_model_registry[model_type](checkpoint=ckpt).to("cuda")
sam = sam_model_registry[model_type](checkpoint=ckpt)
predictor = SamPredictor(sam)
image_np = (image * 255).astype(np.uint8)
@@ -205,45 +206,6 @@ def post_prompt(json_data):
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
@@ -260,8 +222,6 @@ def load_workflow(workflow_name):
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):
@@ -273,8 +233,6 @@ async def post_prompt_block(request):
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")
@@ -292,7 +250,7 @@ async def post_prompt_block(request):
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(f'"{key}"', f'"{mask_path}"')
workflow = workflow.replace("embedding_id_avatech", image_path)
workflow = workflow.replace("SEED", str(randomSeed()))
@@ -319,10 +277,58 @@ async def post_prompt_block(request):
if history:
# file = get_avatar_file(history[prompt_id]["outputs"])
# return web.Response(body=file)
outputs = history[prompt_id]["outputs"]
for node_id, output in outputs.items():
if "gltfFilename" in output:
modelId = upload_avatar_file(output)
print("model id", modelId)
return web.json_response({"id": modelId}, status=200)
time.sleep(0.5)
modelId = upload_avatar_file(history[prompt_id]["outputs"])
print("model id", modelId)
return web.json_response({"id": modelId}, status=200)
# TODO: refactor the code
@server.PromptServer.instance.routes.post("/rendering_generation")
async def post_data_generation(request):
prompt_server = server.PromptServer.instance
post = await request.json()
workflow_name = post.get("workflow_name")
workflow = load_workflow(workflow_name)
workflow = workflow.replace("SEED", str(randomSeed()))
inputs = post.get("inputs")
for key, value in inputs.items():
workflow = workflow.replace(f'"{key}"', f'"{str(value)}"')
res = post_prompt({"prompt": json.loads(workflow)})
prompt_id = json.loads(res.text)["prompt_id"]
while True:
history = prompt_server.prompt_queue.get_history(prompt_id=prompt_id)
if history:
outputs = history[prompt_id]["outputs"]
for node_id, output in outputs.items():
if "images" in output:
filename = output["images"][0]["filename"]
if filename.startswith("rendered"):
return web.json_response({"image": filename}, status=200)
time.sleep(0.5)
@server.PromptServer.instance.routes.post("/image_generation")
async def post_image_generation(request):
prompt_server = server.PromptServer.instance
workflow = load_workflow("generation")
workflow = workflow.replace("SEED", str(randomSeed()))
res = post_prompt({"prompt": json.loads(workflow)})
prompt_id = json.loads(res.text)["prompt_id"]
while True:
history = prompt_server.prompt_queue.get_history(prompt_id=prompt_id)
if history:
outputs = history[prompt_id]["outputs"]
for node_id, output in outputs.items():
if "images" in output:
filename = output["images"][0]["filename"]
if filename.startswith("avatar"):
return web.json_response({"image": filename}, status=200)
time.sleep(0.5)
+27
View File
@@ -0,0 +1,27 @@
import json
class CombinePoints:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
},
}
RETURN_NAMES = ("SAM_PROMPTS",)
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
CATEGORY = "image"
# OUTPUT_NODE = True
def run(self, *args, **kwargs):
sam_prompts = json.dumps(kwargs, default=str)
return (sam_prompts,)
NODE_CLASS_MAPPINGS = {"Combine Points": CombinePoints}
NODE_DISPLAY_NAME_MAPPINGS = {"Combine Points": "Combine Points"}
+66
View File
@@ -0,0 +1,66 @@
import cv2
import numpy as np
import torch
class ExtractBoundaryPoints:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"n_points": ("INT", {"default": -1, "min": -1, "max": 100}),
},
}
RETURN_TYPES = ("POINTS", "IMAGE")
FUNCTION = "run"
CATEGORY = "image"
def find_main_contour(self, image, n_points):
image = np.copy(image[0].numpy())
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
gray = (gray * 255).astype(np.uint8)
# Find contours
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]
max_area_index = areas.index(max(areas))
largest_contour = contours[max_area_index]
if n_points > 0:
divided_by = int(largest_contour.shape[0] / n_points)
divided_by = min(divided_by, largest_contour.shape[0])
largest_contour = largest_contour[::divided_by].astype(int)
contours = [largest_contour]
if not image.flags["C_CONTIGUOUS"]:
image = np.ascontiguousarray(image)
cv2.drawContours(image, contours, -1, (0, 255, 0), 3)
points = []
for point in largest_contour:
points.append(
{"x": point[0][0], "y": point[0][1], "label": 1, "isAuto": True}
)
return image, points
def run(self, image, n_points):
contour_image, points = self.find_main_contour(image, n_points)
contour_image = torch.from_numpy(np.expand_dims(contour_image, axis=0))
print(points)
return (points, contour_image)
NODE_CLASS_MAPPINGS = {"Extract Boundary Points": ExtractBoundaryPoints}
NODE_DISPLAY_NAME_MAPPINGS = {"Extract Boundary Points": "Extract Boundary Points"}
+31
View File
@@ -0,0 +1,31 @@
class LoadValueFromRequest:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"name": (
"STRING",
{"multiline": False, "default": "key_name"},
),
},
"optional": {
"value": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("value",)
FUNCTION = "run"
CATEGORY = "image"
def run(self, name, value=None):
if name:
value = name
return (value,)
NODE_CLASS_MAPPINGS = {"LoadValueFromRequest": LoadValueFromRequest}
NODE_DISPLAY_NAME_MAPPINGS = {"LoadValueFromRequest": "Load Value From Request"}
+20 -7
View File
@@ -4,6 +4,7 @@ import numpy as np
import torch
import re
import json
import uuid
from segment_anything import sam_model_registry, SamPredictor
from einops import rearrange, repeat
from PIL import Image
@@ -126,7 +127,7 @@ class SAMMultiLayer:
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")
sam = sam_model_registry[model_type](checkpoint=ckpt) # .to("cuda")
global_predictor = SamPredictor(sam)
face_landmarker_model_path = os.path.join(
@@ -270,16 +271,24 @@ class SAMMultiLayer:
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
if len(face_landmarks) == 0:
print("Warning: no face detected")
return None, None
pose_landmarks = pose_landmarker.detect(mp_image).pose_landmarks
pose_landmarks = pose_landmarks[0] if len(pose_landmarks) > 0 else None
if len(pose_landmarks) == 0:
print("Warning: no pose detected")
return None, None
imagePromptsMulti, boxesMulti = self.auto_segment(
np_image, face_landmarks, pose_landmarks
np_image, face_landmarks[0], pose_landmarks[0]
)
return imagePromptsMulti, boxesMulti
def load_image(self, image, ckpt, embedding_id, image_prompts_json):
if 'COMFY_DEPLOY' in os.environ and os.getenv('COMFY_DEPLOY', 'FALSE') == "TRUE":
embedding_id = str(uuid.uuid4())
image_prompts = json.loads(image_prompts_json.replace("'", '"'))
order_file = f"{self.output_dir}/segments_{embedding_id}/order.json"
@@ -338,7 +347,7 @@ class SAMMultiLayer:
imagePromptsMulti, boxesMulti = self.detect_face(image[0].numpy())
image_prompts = json.loads(image_prompts_json.replace("'", '"'))
result = [image_prompts]
result = [image_prompts] # use imagePromptsMulti
if isinstance(image_prompts, list):
pass
@@ -350,7 +359,9 @@ class SAMMultiLayer:
continue
points = (
imagePromptsMulti[key] if key in imagePromptsMulti else item
imagePromptsMulti[key]
if imagePromptsMulti is not None and 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])
@@ -358,7 +369,9 @@ class SAMMultiLayer:
masks, _, _ = global_predictor.predict(
point_coords=point_coords,
point_labels=point_labels,
box=boxesMulti[key] if key in boxesMulti else None,
box=boxesMulti[key]
if boxesMulti is not None and key in boxesMulti
else None,
)
masks = torch.from_numpy(masks)
masks = rearrange(masks[0], "h w -> 1 h w")