13 Commits
Author SHA1 Message Date
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
8 changed files with 216 additions and 173 deletions
+25 -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,23 @@ 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",
},],
"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")
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
+3 -46
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",
@@ -711,50 +707,11 @@ export function AvatarPreview() {
return (
(showPreview.val ? "" : "hidden ") +
"w-full h-full absolute left-0 top-0 z-[99] pointer-events-auto flex border-none bg-white"
"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(),
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ export const showEditor = van.state(false);
// 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"
"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);
+136 -70
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.4.0 | MIT License | https://tailwindcss.com
*/
/*
@@ -34,9 +34,11 @@
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html {
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
@@ -46,12 +48,14 @@ html {
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
@@ -123,8 +127,10 @@ strong {
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
@@ -133,8 +139,12 @@ samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
@@ -501,7 +511,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 +752,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 +772,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 +882,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 +970,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)){
@@ -1005,9 +1021,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 +1036,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 +1066,9 @@ html{
.input{
flex-shrink: 1;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
height: 3rem;
padding-left: 1rem;
padding-right: 1rem;
@@ -1152,10 +1171,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)),
@@ -1485,8 +1502,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;
}
@@ -1577,11 +1593,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 +1643,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 +1656,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 +1783,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 +2106,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 +2196,6 @@ details.collapse summary::-webkit-details-marker{
left: 50%;
}
.left-28{
left: 7rem;
}
.left-4{
left: 1rem;
}
@@ -2268,10 +2286,6 @@ details.collapse summary::-webkit-details-marker{
margin-top: 0.5rem;
}
.mt-4{
margin-top: 1rem;
}
.mt-8{
margin-top: 2rem;
}
@@ -2304,19 +2318,43 @@ details.collapse summary::-webkit-details-marker{
height: 394px;
}
.h-full{
height: 100%;
}
.h-fit{
height: -moz-fit-content;
height: fit-content;
}
.h-full{
height: 100%;
.h-\[400px\]{
height: 400px;
}
.h-\[420px\]{
height: 420px;
}
.h-\[410px\]{
height: 410px;
}
.h-\[360px\]{
height: 360px;
}
.min-h-\[400px\]{
min-height: 400px;
}
.min-h-\[380px\]{
min-height: 380px;
}
.min-h-\[350px\]{
min-height: 350px;
}
.w-20{
width: 5rem;
}
@@ -2350,10 +2388,42 @@ details.collapse summary::-webkit-details-marker{
width: 100%;
}
.w-\[\]{
width: ;
}
.w-\[400px\]{
width: 400px;
}
.w-\[420px\]{
width: 420px;
}
.w-\[410px\]{
width: 410px;
}
.w-\[360px\]{
width: 360px;
}
.min-w-\[400px\]{
min-width: 400px;
}
.min-w-\[380\]{
min-width: 380;
}
.min-w-\[380px\]{
min-width: 380px;
}
.min-w-\[350px\]{
min-width: 350px;
}
.max-w-\[100\%\]{
max-width: 100%;
}
@@ -2404,10 +2474,6 @@ details.collapse summary::-webkit-details-marker{
justify-content: space-between;
}
.justify-around{
justify-content: space-around;
}
.gap-2{
gap: 0.5rem;
}
@@ -2424,14 +2490,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 +2514,14 @@ details.collapse summary::-webkit-details-marker{
border-radius: 0.125rem;
}
.rounded-2xl{
border-radius: 1rem;
}
.rounded-xl{
border-radius: 0.75rem;
}
.rounded-b-md{
border-bottom-right-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
@@ -2669,14 +2735,14 @@ details.collapse summary::-webkit-details-marker{
padding: 0.5rem;
}
.p-24{
padding: 6rem;
}
.p-4{
padding: 1rem;
}
.p-24{
padding: 6rem;
}
.\!px-0{
padding-left: 0px !important;
padding-right: 0px !important;
@@ -2692,16 +2758,16 @@ 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;
}
.py-16{
padding-top: 4rem;
padding-bottom: 4rem;
}
.text-start{
text-align: start;
}
+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
+9 -49
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,12 @@ async def post_prompt_block(request):
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)
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)