Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8179d72d9 | ||
|
|
2f38c8467c | ||
|
|
3039963272 | ||
|
|
34fb26d6fb | ||
|
|
cb8b6679da | ||
|
|
8422acfc89 | ||
|
|
c066eea984 | ||
|
|
ce1b4702e0 | ||
|
|
0818a6f1c7 | ||
|
|
80493fa7bc | ||
|
|
4dd87437a4 | ||
|
|
b1f562c8e8 | ||
|
|
c693166d98 | ||
|
|
976ddc8348 | ||
|
|
6cc075422b | ||
|
|
d2c2984555 | ||
|
|
e9ff1e8b64 | ||
|
|
b39bfc0693 | ||
|
|
486000d2b5 | ||
|
|
2907800373 | ||
|
|
e928630a7a | ||
|
|
71d066a8d6 | ||
|
|
3077a2dbae | ||
|
|
323967dc08 | ||
|
|
e4cfb86114 | ||
|
|
788596525e | ||
|
|
3ee3714664 | ||
|
|
cada51a729 | ||
|
|
b7961e025b | ||
|
|
abac0ed6aa | ||
|
|
ff1ed96a74 | ||
|
|
fb81ea509f | ||
|
|
9c7721bb76 | ||
|
|
dfdc97b27b | ||
|
|
07904311ff | ||
|
|
71f6c679b9 | ||
|
|
c121ff37cf | ||
|
|
3931d6c7ed | ||
|
|
71211782aa | ||
|
|
3979064eee | ||
|
|
6f973ecd93 | ||
|
|
acc52d905a | ||
|
|
7803ac467b | ||
|
|
0c20b73d11 | ||
|
|
b2eb735bf4 | ||
|
|
50929a29b8 | ||
|
|
5cb899d9e1 | ||
|
|
ab2b239690 | ||
|
|
7bf1de543f | ||
|
|
850598abe8 | ||
|
|
85b2de6303 | ||
|
|
6854f9b59b | ||
|
|
bb50059376 | ||
|
|
a0f055f0cd | ||
|
|
c5560a8084 | ||
|
|
ab7f660f95 | ||
|
|
6af9e0c62f | ||
|
|
545ec8a3be | ||
|
|
dc97bbc540 | ||
|
|
4432c680cc | ||
|
|
aef869e6d1 | ||
|
|
6810145f0b | ||
|
|
1a39c236ff | ||
|
|
b843eb7c10 | ||
|
|
a48648025d | ||
|
|
8897d5c430 | ||
|
|
9f96f7bff1 | ||
|
|
3510381894 | ||
|
|
f21426106b | ||
|
|
acbb05572c | ||
|
|
56000fbe45 | ||
|
|
4c3864ec4f | ||
|
|
99481e00c0 | ||
|
|
2f0a6f688f | ||
|
|
6133c6c9fb | ||
|
|
c5c080d896 | ||
|
|
f13829a975 | ||
|
|
535e77a0d9 | ||
|
|
035439e1fe | ||
|
|
83a7ce7899 | ||
|
|
0effef3fe2 | ||
|
|
a436e3846d | ||
|
|
83d6a73cfd | ||
|
|
5516baa5c2 | ||
|
|
744cb75552 | ||
|
|
722542e92a | ||
|
|
6e3820b502 | ||
|
|
6ce009d984 | ||
|
|
94874357cd | ||
|
|
d37f9a87f3 | ||
|
|
0729bf982e | ||
|
|
9f1bd85c6b | ||
|
|
082f96262b | ||
|
|
b3340d6085 | ||
|
|
c269a1e340 | ||
|
|
02a6b37971 | ||
|
|
18958a22b0 | ||
|
|
df96815448 | ||
|
|
de211a79e4 | ||
|
|
ae22626e6d | ||
|
|
5313370518 | ||
|
|
b9fe7cce14 | ||
|
|
ad6a2aea56 | ||
|
|
fdc1268ea3 | ||
|
|
b27b6094e2 | ||
|
|
3820d98942 | ||
|
|
723dbe4a71 | ||
|
|
d20d6c1b10 | ||
|
|
2cbf3da2d0 | ||
|
|
3e49f992aa | ||
|
|
f438146927 | ||
|
|
3391bae9ff | ||
|
|
c5b903b9d1 | ||
|
|
834b7abfdf | ||
|
|
a20b23488a | ||
|
|
be5f2c8d1b | ||
|
|
eb2fb63d24 |
@@ -1,9 +1,7 @@
|
|||||||
# Created by https://www.toptal.com/developers/gitignore/api/node,python,react
|
# Created by https://www.toptal.com/developers/gitignore/api/node,python,react
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,python,react
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,python,react
|
||||||
|
|
||||||
workflow_templates/
|
|
||||||
js/output.css
|
js/output.css
|
||||||
*.task
|
|
||||||
|
|
||||||
### Node ###
|
### Node ###
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
@@ -2,168 +2,211 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Wanna animate or got a question? Join our [Discord](https://discord.gg/Xp6mZ4Ez5P)
|
|
||||||
|
|
||||||
A custom nodes module for **creating real-time interactive avatars** powered by blender bpy mesh api + Avatech Shape Flow runtime.
|
A custom nodes module for **creating real-time interactive avatars** powered by blender bpy mesh api + Avatech Shape Flow runtime.
|
||||||
|
|
||||||
> **WARNING**
|
|
||||||
> We are still making changes to the nodes and demo templates, please stay tuned.
|
|
||||||
|
|
||||||
# Demo
|
# Demo
|
||||||
|
|
||||||
| <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/12e2bfc6-438e-4d16-bead-9957ced3bae1" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=cce15b92-6d1c-4966-91b9-362d7833cb5d) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/0c497025-7ed5-4e25-b4d1-5a257e1ba814" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=42a8182f-b140-48c0-a556-35cddf0f76f7) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/a2bf71e3-0d9c-4ddd-957f-a6b0cb7e622a" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=7c23b8d6-d1a5-41c7-a084-250461dbef22) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/ad808c42-5297-4e61-8be8-d5cb7729d2ff" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=268b32c4-f9b9-4db8-a27c-a7e974f0f0ac) |
|
| <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/12e2bfc6-438e-4d16-bead-9957ced3bae1" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=cce15b92-6d1c-4966-91b9-362d7833cb5d) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/0c497025-7ed5-4e25-b4d1-5a257e1ba814" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=42a8182f-b140-48c0-a556-35cddf0f76f7) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/a2bf71e3-0d9c-4ddd-957f-a6b0cb7e622a" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=7c23b8d6-d1a5-41c7-a084-250461dbef22) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/ad808c42-5297-4e61-8be8-d5cb7729d2ff" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=268b32c4-f9b9-4db8-a27c-a7e974f0f0ac) |
|
||||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
|:---:|:---:|:---:|:---:|
|
||||||
| <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/1d1ad8f9-31a6-48ec-bad2-ce972ee3b12f" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=f97fc5bb-93b0-4b02-bbc0-327dd41d0fc5) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/06958585-f780-4b38-8f5d-bddabd7da78a" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=4d50aa03-26e4-47e7-97b6-c3fe9d8fc96e) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/3d0e6b54-d45f-45ac-90bd-d8b149880f98" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=791014cb-7836-4641-afdb-ac331064b682) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/1d1ad8f9-31a6-48ec-bad2-ce972ee3b12f" width="180"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=f97fc5bb-93b0-4b02-bbc0-327dd41d0fc5) |
|
| <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/1d1ad8f9-31a6-48ec-bad2-ce972ee3b12f" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=f97fc5bb-93b0-4b02-bbc0-327dd41d0fc5) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/06958585-f780-4b38-8f5d-bddabd7da78a" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=4d50aa03-26e4-47e7-97b6-c3fe9d8fc96e) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/3d0e6b54-d45f-45ac-90bd-d8b149880f98" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=791014cb-7836-4641-afdb-ac331064b682) | <img src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/1d1ad8f9-31a6-48ec-bad2-ce972ee3b12f" width="190"/><br>[Interact 👆](https://editor.avatech.ai/viewer?avatarId=f97fc5bb-93b0-4b02-bbc0-327dd41d0fc5) |
|
||||||
|
|
||||||
# How to?
|
|
||||||
|
|
||||||
- [Basic Rigging Workflow Template](#basic-rigging-workflow-template)
|
# Contents
|
||||||
- [Best Practices for image input](#best-practices-for-image-input)
|
- [Workflow Template](workflow-template)
|
||||||
- [Custom Nodes List](#custom-nodes)
|
- [Template01 - Simple Shape Flow](#template01---simple-shape-flow)
|
||||||
|
- [Image Preprocess Guide](#image-preprocess-guide)
|
||||||
|
- [Character Gen Prompting Guide](#character-gen-prompting-guide)
|
||||||
|
- [Mouth Open Guide (Inpaint)](#mouth-open-guide-inpaint)
|
||||||
|
- [Custom Nodes](custom-nodes)
|
||||||
|
- [Image Segmentation Nodes](#image-segmentation-nodes)
|
||||||
|
- [Mesh Edit Nodes](#mesh-edit-nodes)
|
||||||
|
- [Shape Keys Nodes](#shape-keys-nodes)
|
||||||
|
- [Avatar Output Nodes](#avatar-output-nodes)
|
||||||
- [Shape Flow](#shape-flow)
|
- [Shape Flow](#shape-flow)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Join Discord 💬](https://discord.gg/WNtBYksDwF)
|
|
||||||
|
|
||||||
# Basic Rigging Workflow Template
|
# Workflow Template
|
||||||
|
|
||||||
### 1. Creating an eye blink and lipsync avatar
|
## Template01 - Simple Shape Flow
|
||||||
|
To enable the character to blink eyes and talking.
|
||||||
|
|
||||||

|
> **🎯Notice**
|
||||||
|
>
|
||||||
|
> For optimal results, please input a character image with an open mouth and a minimum resolution of 768x768. This higher resolution will enable the tool to accurately recognize and work with facial features.
|
||||||
|
|
||||||
For optimal results, please input a character image with an open mouth and a minimum resolution of 768x768. This higher resolution will enable the tool to accurately recognize and work with facial features.
|
[💡Generate new image Guide](#character-gen-prompting-guide)
|
||||||
|
|
||||||
Download: Save the image, and drag into Comfyui or [Simple Shape Flow](https://github.com/avatechai/avatar-graph-comfyui/blob/main/workflow_templates/SimpleEye+MouthMovement.json)
|
[💡Make your character mouth open Guide](#mouth-open-guide-inpaint)
|
||||||
|
|
||||||
### 2. Creating an eye blink and lipsync emoji avatar
|

|
||||||
|
|
||||||
|  |  |
|
### Download: 📂[Template01 - Simple Shape Flow](https://github.com/avatechai/avatar-graph-comfyui/blob/main/workflow_templates/SimpleEye+MouthMovement.json)
|
||||||
| :--: | :--: |
|
### Download: 📂[Template01 - ControlNet Gen](https://github.com/avatechai/avatar-graph-comfyui/tree/main/workflow_templates/TemplateGen01)
|
||||||
|
_If you don't want to modify any values in the custom nodes, you can download the ControlNet Gen Template to generate your own image._
|
||||||
|
|
||||||
Download: Save the image, and drag into Comfyui
|
<details>
|
||||||
|
<summary> Template01 - Nodes Value Setting Guide </summary>
|
||||||
|
|
||||||
|  |  |
|
## Template01 - Nodes Value Setting Guide
|
||||||
| :--: | :--: |
|
|
||||||
|
|
||||||
Download: Save the image, and drag into Comfyui or [Dog Workflow](https://github.com/avatechai/avatar-graph-comfyui/blob/main/workflow_templates/Dog_workflow.json)
|
> ### Basic Eyeblink & Talking
|
||||||
|
> 1. Click **[Segmentation (SAM)]/ Edit prompt** button
|
||||||
# Best practices for image input
|
>
|
||||||
|
> 2. Add new layer and rename
|
||||||
### 1. Generate a new character image
|
>
|
||||||
|
> 3. Drag layer to **[Create Mesh Layer]/image**
|
||||||

|
>
|
||||||
|
> 4. **[Create Mesh Layer]/ face_threshold, shape_threshold**, To control mesh threshold, recommend value: 0.6~0.7
|
||||||
We need a character image with an open mouth and enable the tool to easily recognize facial features, so please add to the prompt:
|
>
|
||||||
`looking at viewer, detailed face, open mouth, [smile], solo,eye-level angle`
|
> 5. **[Create Mesh Layer]/ scale_x, scale_y, extrude_x, extrude_y**, To control mesh threshold, recommend value: 1.2~1.4
|
||||||
|
>
|
||||||
Download: [Character Gen Template](https://github.com/avatechai/avatar-graph-comfyui/blob/main/workflow_templates/SimpleCharacterGen.json)
|
> 6. **[Modify Shape Key]/ rotate** Setting Reference, If Head tilted to the left, set a positive number angle
|
||||||
|
>
|
||||||
### 2. Make existing character image mouth open (Inpaint)
|
> | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/271264869-abf2a843-8ca5-44a6-9611-c334d55928d1.png" width="300"> | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/271264902-37658a8e-6f46-4c5b-bfd6-adec270df60b.png" width="300"> | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/271264910-0fae0c27-428d-4a5d-8296-6634c9717b95.png" width="300"> | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/271264920-4fea7882-cc51-4a5a-af9a-e66589810f92.png" width="300"> |
|
||||||
|
> | --- | --- | --- | --- |
|
||||||

|
> | 0 | 5 | -5 | -15 |
|
||||||
|
|
||||||
To maintain consistency with the base image, it is recommended to utilize a checkpoint model that aligns with its style.
|
|
||||||
|
|
||||||
Download: [Mouth Open Inpaint Template](<https://github.com/avatechai/avatar-graph-comfyui/blob/main/workflow_templates/MouthOpen_(inpaint).json>)
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary> Inpaint Demonstration </summary>
|
|
||||||
|
|
||||||
<video src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/e3b77295-a1bf-4d96-9551-7cc423a4af73"/>
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 3. Pose Constraints (ControlNet)
|
<details>
|
||||||
|
|
||||||

|
<summary> Template01 - ControlNet Gen Guide </summary>
|
||||||
|
|
||||||
Place normal and openpose image with reference to images.
|
Place normal and openpose image with reference to images.
|
||||||
|
|
||||||
Download: [ControlNet Gen](https://github.com/avatechai/avatar-graph-comfyui/tree/main/workflow_templates/TemplateGen01)
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
# Recommend Checkpoint Model List
|
|
||||||
|
# Image Preprocess Guide
|
||||||
|
|
||||||
##### Anime Style SD1.5
|
### 💡If you want to generate a new character image
|
||||||
|
> you can download this Template and refer to the Guide!
|
||||||
|
> <details>
|
||||||
|
> <summary> Character Gen Prompting Guide </summary>
|
||||||
|
>
|
||||||
|
> # Character Gen Prompting Guide
|
||||||
|
>> **🎯Notice**
|
||||||
|
>>
|
||||||
|
>> We need a character image with an open mouth and enable the tool to easily recognize facial features, so please add to the prompt:
|
||||||
|
>>
|
||||||
|
>> ```looking at viewer, detailed face, open mouth, [smile], solo,eye-level angle```
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
> ### Download: 📂[Character Gen Template](https://github.com/avatechai/avatar-graph-comfyui/blob/main/workflow_templates/SimpleCharacterGen.json)
|
||||||
|
> Feel free to change any checkpoint model that suits your needs.
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
|
||||||
- https://civitai.com/models/35960/flat-2d-animerge
|
### 💡If you have a character image but it's not mouth open
|
||||||
- https://civitai.com/models/24149/mistoonanime
|
> you can download this Template and refer to the Guide!
|
||||||
|
> <details>
|
||||||
##### Realistic Style SD1.5
|
> <summary> Mouth Open Guide (Inpaint) </summary>
|
||||||
|
>
|
||||||
- https://civitai.com/models/4201/realistic-vision-v51
|
> # Mouth Open Guide (Inpaint)
|
||||||
- https://civitai.com/models/49463/am-i-real
|
> To maintain consistency with the base image, it is recommended to utilize a checkpoint model that aligns with its style.
|
||||||
- https://civitai.com/models/43331/majicmix-realistic
|
>
|
||||||
|
> 
|
||||||
|
>
|
||||||
|
> ### Download: 📂[MouthOpen Template](https://github.com/avatechai/avatar-graph-comfyui/blob/main/workflow_templates/MouthOpen_(inpaint).json)
|
||||||
|
>
|
||||||
|
> ### Inpaint Demonstration
|
||||||
|
>
|
||||||
|
> <video src="https://github.com/avatechai/avatar-graph-comfyui/assets/73209427/e3b77295-a1bf-4d96-9551-7cc423a4af73"/>
|
||||||
|
>
|
||||||
|
> ### Recommend Checkpoint Model List
|
||||||
|
>
|
||||||
|
> ##### Anime Style SD1.5
|
||||||
|
>- https://civitai.com/models/35960/flat-2d-animerge
|
||||||
|
>- https://civitai.com/models/24149/mistoonanime
|
||||||
|
>- https://civitai.com/models/22364/kizuki-anime-hentai-checkpoint
|
||||||
|
>##### Realistic Style SD1.5
|
||||||
|
>- https://civitai.com/models/4201/realistic-vision-v51
|
||||||
|
>- https://civitai.com/models/49463/am-i-real
|
||||||
|
>- https://civitai.com/models/43331/majicmix-realistic
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
|
||||||
# Custom Nodes
|
# Custom Nodes
|
||||||
|
Expand to see all the available nodes description
|
||||||
Expand to see all the available nodes description.
|
Mesh Edit Nodes
|
||||||
|
Shape Keys Nodes
|
||||||
|
Avatar Output Nodes
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> All Custom Nodes </summary>
|
<summary> Image Segmentation Nodes </summary>
|
||||||
|
|
||||||
| Name | Description | Preview |
|
## Image Segmentation Nodes
|
||||||
| -------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| Name | Description | Preview |
|
||||||
| `Segmentation (SAM)` | Integrative SAM node allowing you to directly select and create multiple image segment output. | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270576351-8aabeba8-5450-4d39-8203-e91f9ab47190.png" width="300"> |
|
| ---------------------------- | ------------ | ------- |
|
||||||
|
| `Segmentation (SAM)` | Integrative SAM node allowing you to directly select and create multiple image segment output. | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270576351-8aabeba8-5450-4d39-8203-e91f9ab47190.png" width="300"> |
|
||||||
|
|
||||||
| Name | Description | Preview |
|
|
||||||
| ---------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `Create Mesh Layer` | Create a mesh object from the input images (usually a segmented part of the entire image) | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270576646-40740d25-9411-4cd3-a6c0-8b9008bca41c.png" width="300"> |
|
|
||||||
| `Join Meshes` | Combine multiple meshes into a single mesh object | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270577004-ba7afbc5-9cd5-4f97-9614-f71133f5783e.png" width="300"> |
|
|
||||||
| `Match Texture Aspect Ratio` | Since the mesh is created in 1:1 aspect ratio, a re-scale is needed at the end of the operation | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270602782-cb7155be-fb31-49f8-a24a-d001a1484ea7.png" width="300"> |
|
|
||||||
| `Plane Texture Unwrap` | Will perform mesh face fill and UV Cube project on the target plane mesh, scaled to bounds. | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270603006-4b9c0cf5-0497-47bf-8e06-5a3370084c11.png" width="300"> |
|
|
||||||
|
|
||||||
| Name | Description | Preview |
|
|
||||||
| ----------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `Mesh Modify Shape Key` | Given shape key name & target vertex_group, modify the vertex / all vertex’s transform | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270577944-ab4f259c-89a7-4f51-bc54-fd179e252073.png" width="300"> |
|
|
||||||
| `Create Shape Flow` | Create runtime shape flow graph, allowing interactive inputs affecting shape keys value in runtime | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270592752-abfdd801-0387-4c5d-9c11-6c23337ff1dd.png" width="300"> |
|
|
||||||
|
|
||||||
| Name | Description | Preview |
|
|
||||||
| -------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `Avatar Main Output` | The primary output of the .ava file. The embedded Avatar View will auto update with this node's output | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270592519-6a9a8bb4-05ec-4a2e-98bf-194b6af3a62a.png" width="300"> |
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
# Shape Flow
|
<details>
|
||||||
|
<summary> Mesh Edit Nodes </summary>
|
||||||
|
|
||||||
|
## Mesh Edit Nodes
|
||||||
|
|
||||||
|
| Name | Description | Preview |
|
||||||
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `Create Mesh Layer` | Create a mesh object from the input images (usually a segmented part of the entire image) | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270576646-40740d25-9411-4cd3-a6c0-8b9008bca41c.png" width="300"> |
|
||||||
|
| `Join Meshes` | Combine multiple meshes into a single mesh object | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270577004-ba7afbc5-9cd5-4f97-9614-f71133f5783e.png" width="300"> |
|
||||||
|
| `Match Texture Aspect Ratio` | Since the mesh is created in 1:1 aspect ratio, a re-scale is needed at the end of the operation | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270602782-cb7155be-fb31-49f8-a24a-d001a1484ea7.png" width="300"> |
|
||||||
|
| `Plane Texture Unwrap` | Will perform mesh face fill and UV Cube project on the target plane mesh, scaled to bounds. | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270603006-4b9c0cf5-0497-47bf-8e06-5a3370084c11.png" width="300"> |
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> Shape Keys Nodes </summary>
|
||||||
|
|
||||||
|
## Shape Keys Nodes
|
||||||
|
| Name | Description | Preview |
|
||||||
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `Mesh Modify Shape Key` | Given shape key name & target vertex_group, modify the vertex / all vertex’s transform | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270577944-ab4f259c-89a7-4f51-bc54-fd179e252073.png" width="300"> |
|
||||||
|
| `Create Shape Flow` | Create runtime shape flow graph, allowing interactive inputs affecting shape keys value in runtime | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270592752-abfdd801-0387-4c5d-9c11-6c23337ff1dd.png" width="300"> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> Avatar Output Nodes </summary>
|
||||||
|
|
||||||
|
## Avatar Output Nodes
|
||||||
|
| Name | Description | Preview |
|
||||||
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `Avatar Main Output` | The primary output of the .ava file. The embeded Avatar View will auto update with this node's output | <img src="https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/public-download/github-readme/270592519-6a9a8bb4-05ec-4a2e-98bf-194b6af3a62a.png" width="300"> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Shape Flow
|
||||||

|

|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
## Method 1 - Windows
|
Clone the repository to custom_nodes in your [ComfyUI](https://github.com/comfyanonymous/ComfyUI) directory:
|
||||||
|
|
||||||
1. Download Python environment from [here](https://avatech-avatar-dev1.nyc3.digitaloceanspaces.com/comfyui/ComfyUI_3.10.7z)
|
|
||||||
|
|
||||||
2. Unzip it to ComfyUI directory
|
|
||||||
|
|
||||||
3. Run the `run_cpu_3.10.bat` or `run_nvidia_gpu_3.10.bat`
|
|
||||||
|
|
||||||
4. Install avatar-graph-comfyui from [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager)
|
|
||||||
|
|
||||||
## Method 2 - macOS/Linux
|
|
||||||
|
|
||||||
Make sure your Python environment is `3.10.x` as required by the [bpy](https://pypi.org/project/bpy/) package. Then go to the [ComfyUI](https://github.com/comfyanonymous/ComfyUI) directory and run:
|
|
||||||
|
|
||||||
> Suggest using conda for your comfyui python environment
|
|
||||||
>
|
|
||||||
> `conda create --name comfyui python=3.10`
|
|
||||||
>
|
|
||||||
> `conda activate comfyui`
|
|
||||||
>
|
|
||||||
> `pip install -r requirements.txt`
|
|
||||||
|
|
||||||
1. `cd custom_nodes`
|
1. `cd custom_nodes`
|
||||||
|
|
||||||
2. `git clone https://github.com/avatechgg/avatar-graph-comfyui.git`
|
2. `git clone https://github.com/avatechgg/avatar-graph-comfyui.git`
|
||||||
|
|
||||||
3. `cd avatar-graph-comfyui && python -m pip install -r requirements.txt`
|
3. Install deps `cd avatar-graph-comfyui && python -m pip install -r requirements.txt`
|
||||||
|
|
||||||
4. Restart ComfyUI with enable-cors-header `python main.py --enable-cors-header` or (for mac) `python main.py --force-fp16 --enable-cors-header`
|
4. Restart comfyui
|
||||||
|
|
||||||
|
5. Run comfyui with enable-cors-header `python main.py --enable-cors-header` or (mac)`python main.py --force-fp16 --enable-cors-header`
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
If you are interested in contributing
|
<details>
|
||||||
|
<summary> If you are interested in contributing expand to see development details </summary>
|
||||||
|
|
||||||
|
|
||||||
For comfyui frontend extension, frontend js located at `avatar-graph-comfyui/js`
|
For comfyui frontend extension, frontend js located at `avatar-graph-comfyui/js`
|
||||||
|
|
||||||
@@ -193,21 +236,9 @@ For each changes, simply refresh the comfyui page to see the changes.
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Update blender node types
|
</details>
|
||||||
|
|
||||||
To update blender operations input and output types (stored in `blender/input_types.txt`), run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python generate_blender_types.py
|
|
||||||
```
|
|
||||||
|
|
||||||
# FAQ
|
|
||||||
|
|
||||||
## What is `--enable-cors-header` used for?
|
|
||||||
It is used to enable communication between ComfyUI and our editor (https://editor.avatech.ai), which is in charge of animating static characters. The only messages exchanged between them are the character data like the meshes of eyes and mouth, and the JSON format of our editor graph.
|
|
||||||
|
|
||||||
When you execute the ComfyUI graph, it sends the character data and the JSON graph to our editor for animating. When you modify and save the graph in our editor, it sends the modified graph back to ComfyUI. To validate it, you can open the `js/index.js`, and log the message in `window.addEventListener("message", ...)` and `postMessage(message)`.
|
|
||||||
|
|
||||||
You can also run ComfyUI *without* the `--enable-cors-header`: execute the ComfyUI workflow, then download the .GLB or .GLTF format by right clicking the Avatar Main Output node and Save File option. Yet, this will disable the real-time character preview in the top-right corner of ComfyUI. Feel free to view it in other software like Blender.
|
|
||||||
|
|||||||
+17
-47
@@ -9,6 +9,7 @@ import sys
|
|||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__)))
|
sys.path.append(os.path.join(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
import routes
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
import importlib
|
import importlib
|
||||||
@@ -42,29 +43,7 @@ def append_to_sys_path(path):
|
|||||||
if path not in sys.path:
|
if path not in sys.path:
|
||||||
sys.path.append(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():
|
def download_sam_model():
|
||||||
model_dir = get_folder_paths("sams")[0]
|
model_dir = get_folder_paths("sams")[0]
|
||||||
@@ -74,35 +53,27 @@ def download_sam_model():
|
|||||||
add_model_folder_path("sams", model_dir)
|
add_model_folder_path("sams", model_dir)
|
||||||
|
|
||||||
files = get_filename_list("sams")
|
files = get_filename_list("sams")
|
||||||
if "sam_vit_h_4b8939.pth" not in files:
|
if len(files) == 0:
|
||||||
print("Downloading sam model...")
|
print("Downloading sam model...")
|
||||||
url = "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth"
|
url = "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth"
|
||||||
download_model(url, f"{model_dir}/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_sam_model()
|
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"]
|
paths = ["blender", "sam"]
|
||||||
files = []
|
files = []
|
||||||
|
|
||||||
@@ -114,7 +85,6 @@ for path in paths:
|
|||||||
NODE_CLASS_MAPPINGS = {}
|
NODE_CLASS_MAPPINGS = {}
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||||
|
|
||||||
import routes
|
|
||||||
import blender_node
|
import blender_node
|
||||||
|
|
||||||
base_class = blender_node.ObjectOps
|
base_class = blender_node.ObjectOps
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import platform
|
import platform
|
||||||
import blender_node
|
import blender_node
|
||||||
from mesh_utils import upload_avatar_file, open_in_blender as open_blender, export_gltf
|
from mesh_utils import assign_texture, open_in_blender as open_blender, export_gltf
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
|
||||||
global_blender_path = ''
|
global_blender_path = ''
|
||||||
@@ -47,16 +47,14 @@ class AvatarMainOutput(blender_node.ObjectOps):
|
|||||||
}),
|
}),
|
||||||
"model_type": (["AVA","GLB", "GLTF_EMBEDDED"],),
|
"model_type": (["AVA","GLB", "GLTF_EMBEDDED"],),
|
||||||
"write_mode": (["Overwrite", "Increment"],),
|
"write_mode": (["Overwrite", "Increment"],),
|
||||||
"upload_to_cloud": ("BOOLEAN", {
|
|
||||||
"default": False
|
|
||||||
}),
|
|
||||||
"SHAPE_FLOW": ("SHAPE_FLOW",),
|
"SHAPE_FLOW": ("SHAPE_FLOW",),
|
||||||
}
|
}
|
||||||
|
|
||||||
OUTPUT_NODE = True
|
OUTPUT_NODE = True
|
||||||
RETURN_TYPES = ()
|
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='', upload_to_cloud=False, 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='', SHAPE_FLOW=''):
|
||||||
if open_in_blender:
|
if open_in_blender:
|
||||||
p = blender_path_override if blender_path_override else global_blender_path
|
p = blender_path_override if blender_path_override else global_blender_path
|
||||||
output_file = self.output_dir + '/tmp.blend'
|
output_file = self.output_dir + '/tmp.blend'
|
||||||
@@ -74,24 +72,4 @@ class AvatarMainOutput(blender_node.ObjectOps):
|
|||||||
import global_bpy
|
import global_bpy
|
||||||
global_bpy.set_should_reset_scene(True)
|
global_bpy.set_should_reset_scene(True)
|
||||||
|
|
||||||
outputs = {
|
return {"ui": {"gltfFilename": {filepath.replace(f"{self.output_dir}/", "")}, "SHAPE_FLOW": {SHAPE_FLOW}, "auto_save": {'true' if auto_save else 'false'},}}
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ class VECTOR3D:
|
|||||||
"required": {
|
"required": {
|
||||||
"x": ("FLOAT", {
|
"x": ("FLOAT", {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"step": 0.01,
|
"step": 0.1,
|
||||||
"display": "number"
|
"display": "number"
|
||||||
}),
|
}),
|
||||||
"y": ("FLOAT", {
|
"y": ("FLOAT", {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"step": 0.01,
|
"step": 0.1,
|
||||||
"display": "number"
|
"display": "number"
|
||||||
}),
|
}),
|
||||||
"z": ("FLOAT", {
|
"z": ("FLOAT", {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"step": 0.01,
|
"step": 0.1,
|
||||||
"display": "number"
|
"display": "number"
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ class VECTOR4D:
|
|||||||
"required": {
|
"required": {
|
||||||
"x": ("FLOAT", {
|
"x": ("FLOAT", {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"step": 0.01,
|
"step": 0.1,
|
||||||
"display": "number"
|
"display": "number"
|
||||||
}),
|
}),
|
||||||
"y": ("FLOAT", {
|
"y": ("FLOAT", {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"step": 0.01,
|
"step": 0.1,
|
||||||
"display": "number"
|
"display": "number"
|
||||||
}),
|
}),
|
||||||
"z": ("FLOAT", {
|
"z": ("FLOAT", {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"step": 0.01,
|
"step": 0.1,
|
||||||
"display": "number"
|
"display": "number"
|
||||||
}),
|
}),
|
||||||
"u": ("FLOAT", {
|
"u": ("FLOAT", {
|
||||||
|
|||||||
+18
-28
@@ -1,7 +1,6 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
|
|
||||||
BPY_OBJS = "BPY_OBJS"
|
BPY_OBJS = "BPY_OBJS"
|
||||||
BPY_OBJ = "BPY_OBJ"
|
BPY_OBJ = "BPY_OBJ"
|
||||||
@@ -11,13 +10,12 @@ BPY_OBJS_TYPE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node_input_types = {}
|
node_input_types = {}
|
||||||
with open(f"{os.path.dirname(__file__)}/input_types.txt") as f:
|
with open("custom_nodes/avatar-graph-comfyui/blender/input_types.txt") as f:
|
||||||
input_types = f.readlines()
|
input_types = f.readlines()
|
||||||
for input_type in input_types:
|
for input_type in input_types:
|
||||||
node_cls, node_types = input_type.split("|")
|
node_cls, node_types = input_type.split("|")
|
||||||
node_input_types[node_cls] = json.loads(node_types)
|
node_input_types[node_cls] = json.loads(node_types)
|
||||||
|
|
||||||
type_generation = os.getenv('TYPE_GENERATION', 0)
|
|
||||||
|
|
||||||
class ObjectOps:
|
class ObjectOps:
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -49,29 +47,22 @@ class ObjectOps:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
if type_generation:
|
return node_input_types[cls.__name__]
|
||||||
import global_bpy
|
|
||||||
|
|
||||||
bpy = global_bpy.get_bpy()
|
# import global_bpy
|
||||||
result = {
|
# bpy = global_bpy.get_bpy()
|
||||||
"required": {},
|
# result = {
|
||||||
"optional": {
|
# "required": {},
|
||||||
**cls.get_base_input_types(bpy),
|
# "optional": {
|
||||||
**cls.get_extra_input_types(bpy),
|
# **cls.get_base_input_types(bpy),
|
||||||
},
|
# **cls.get_extra_input_types(bpy)
|
||||||
}
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
return result
|
# with open("input_types.txt", "a") as f:
|
||||||
elif cls.__name__ in node_input_types:
|
# f.write(cls.__name__ + "|" + json.dumps(result) + "\n")
|
||||||
return node_input_types[cls.__name__]
|
|
||||||
else:
|
# return result
|
||||||
return {
|
|
||||||
"required": {},
|
|
||||||
"optional": {
|
|
||||||
**cls.get_base_input_types(None),
|
|
||||||
**cls.get_extra_input_types(None),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def NODE_CLASS_MAPPINGS(cls):
|
def NODE_CLASS_MAPPINGS(cls):
|
||||||
@@ -100,14 +91,14 @@ class ObjectOps:
|
|||||||
import global_bpy
|
import global_bpy
|
||||||
bpy = global_bpy.get_bpy()
|
bpy = global_bpy.get_bpy()
|
||||||
|
|
||||||
if props.get("BPY_OBJ") is not None:
|
if props.get("BPY_OBJ") != None:
|
||||||
bpy.context.view_layer.objects.active = props["BPY_OBJ"]
|
bpy.context.view_layer.objects.active = props["BPY_OBJ"]
|
||||||
|
|
||||||
results = self.blender_process(bpy, **props)
|
results = self.blender_process(bpy, **props)
|
||||||
|
|
||||||
if results is None:
|
if results is None:
|
||||||
# print(results)
|
# print(results)
|
||||||
if props.get("BPY_OBJ") is not None:
|
if props.get("BPY_OBJ") != None:
|
||||||
return (props["BPY_OBJ"], )
|
return (props["BPY_OBJ"], )
|
||||||
else:
|
else:
|
||||||
return (bpy.context.view_layer.objects.active, )
|
return (bpy.context.view_layer.objects.active, )
|
||||||
@@ -260,8 +251,7 @@ def create_primitive_shape_class(cls, path, name=None, name_prefix=''):
|
|||||||
|
|
||||||
|
|
||||||
def assign_and_return(BPY_OBJ, name, value):
|
def assign_and_return(BPY_OBJ, name, value):
|
||||||
setattr(BPY_OBJ, name, value)
|
BPY_OBJ[name] = value
|
||||||
# BPY_OBJ[name] = value
|
|
||||||
# print(BPY_OBJ,name, BPY_OBJ[name])
|
# print(BPY_OBJ,name, BPY_OBJ[name])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import folder_paths
|
|
||||||
import os
|
|
||||||
from PIL import Image, ImageOps
|
|
||||||
from PIL.PngImagePlugin import PngInfo
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
import json
|
|
||||||
|
|
||||||
class ImageAlphaMaskMerge:
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(s):
|
|
||||||
return {"required":
|
|
||||||
{"image": ("IMAGE",) ,
|
|
||||||
"mask": ("MASK",) },
|
|
||||||
}
|
|
||||||
|
|
||||||
CATEGORY = "image"
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "load_image"
|
|
||||||
def load_image(self, image, mask):
|
|
||||||
if image.shape[1] == mask.shape[0] and image.shape[2] == mask.shape[1]:
|
|
||||||
image = torch.cat((image, 1 - mask.unsqueeze(0).unsqueeze(3)), dim=3)
|
|
||||||
return (image, )
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"Image Alpha Mask Merge": ImageAlphaMaskMerge,
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
||||||
"Image Alpha Mask Merge": "Image Alpha Mask Merge"
|
|
||||||
}
|
|
||||||
+18
-1
@@ -1,3 +1,17 @@
|
|||||||
|
Object_VertexGroupNewWithName|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "name": ["STRING", {"multiline": false, "default": "Group"}], "assign_selected": ["BOOLEAN", {"default": true}]}}
|
||||||
|
EditOps|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"]}}
|
||||||
|
ContextSet_TransformPivotPoint|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "pivot": [["BOUNDING_BOX_CENTER", "CURSOR", "INDIVIDUAL_ORIGINS", "MEDIAN_POINT", "ACTIVE_ELEMENT"]]}}
|
||||||
|
Object_AddShapeKeys|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "shape_keys": ["STRING", {"default": "key1,key2", "multiline": true}], "from_mix": ["BOOLEAN", {"default": false}]}}
|
||||||
|
Object_MeshFromTexture|{"required": {}, "optional": {"image": ["IMAGE"], "seed": ["INT", {"default": 0, "min": 0, "max": 18446744073709551615}]}}
|
||||||
|
Object_MatchTextureAspectRatio|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "image": ["IMAGE"], "scale": ["FLOAT", {"default": 0.001, "display": "number", "step": 0.001}]}}
|
||||||
|
AssignVertexGroupOps|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "name": ["STRING", {"multiline": false, "default": "Group"}]}}
|
||||||
|
Object_VertexGroupNewWithName|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "name": ["STRING", {"multiline": false, "default": "Group"}], "assign_selected": ["BOOLEAN", {"default": true}]}}
|
||||||
|
Object_CreateMeshLayer|{"required": {}, "optional": {"image": ["IMAGE"], "convex_hull": ["BOOLEAN", {"default": true}], "shape_threshold": ["FLOAT", {"display": "number", "default": 0.7}], "mesh_layer_name": ["STRING", {"default": "mesh_layer"}], "scale_x": ["FLOAT", {"display": "number", "default": 1}], "scale_y": ["FLOAT", {"display": "number", "default": 1}], "extrude_x": ["FLOAT", {"display": "number", "default": 0}], "extrude_y": ["FLOAT", {"display": "number", "default": 0}], "seed": ["INT", {"default": 0, "min": 0, "max": 18446744073709551615}]}}
|
||||||
|
GetImageWidthHeight|{"required": {}, "optional": {"image": ["IMAGE"], "scale": ["FLOAT", {"default": 1.0}]}}
|
||||||
|
GetFirstObjOps|{"required": {}, "optional": {"BPY_OBJS": ["BPY_OBJS"]}}
|
||||||
|
Object_AssignTexture|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "texture": ["IMAGE"], "texture_name": ["STRING", {"multiline": false, "default": "my_image"}]}}
|
||||||
|
Mesh_JoinMesh|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "BPY_OBJ2": ["BPY_OBJ"]}}
|
||||||
|
Mesh_ModifyShapeKey|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "shape_key_name": ["STRING", {"multiline": false, "default": "EyeBlinkLeft"}], "target_vertex_group": ["STRING", {"multiline": false, "default": ""}], "scale_x": ["FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}], "scale_y": ["FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}], "offset_x": ["FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}], "offset_y": ["FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}], "rotate": ["FLOAT", {"default": 0, "min": -360, "max": 360.0, "step": 0.01, "display": "number"}], "origin_offset_x": ["FLOAT", {"default": 0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}], "origin_offset_y": ["FLOAT", {"default": 0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}], "transform_radius": ["FLOAT", {"default": 1.0, "min": 0, "max": 1, "step": 0.01, "display": "number"}], "falloff": ["FLOAT", {"default": 0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}]}}
|
||||||
Mesh_AttributeSet|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "value_float": ["FLOAT", {"min": -3.4028234663852886e+38, "max": 3.4028234663852886e+38, "default": 0.0}], "value_float_vector_2d": ["B_VECTOR2", {}], "value_float_vector_3d": ["B_VECTOR3", {}], "value_int": ["INT", {"min": -2147483648, "max": 2147483647, "default": 0}], "value_int_vector_2d": ["B_VECTOR2", {}], "value_color": ["B_VECTOR4", {}], "value_bool": ["BOOLEAN", {"default": false}]}}
|
Mesh_AttributeSet|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "value_float": ["FLOAT", {"min": -3.4028234663852886e+38, "max": 3.4028234663852886e+38, "default": 0.0}], "value_float_vector_2d": ["B_VECTOR2", {}], "value_float_vector_3d": ["B_VECTOR3", {}], "value_int": ["INT", {"min": -2147483648, "max": 2147483647, "default": 0}], "value_int_vector_2d": ["B_VECTOR2", {}], "value_color": ["B_VECTOR4", {}], "value_bool": ["BOOLEAN", {"default": false}]}}
|
||||||
Mesh_AverageNormals|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "average_type": [["CUSTOM_NORMAL", "FACE_AREA", "CORNER_ANGLE"]], "weight": ["INT", {"min": 1, "max": 100, "default": 50}], "threshold": ["FLOAT", {"min": 0.0, "max": 10.0, "default": 0.009999999776482582}]}}
|
Mesh_AverageNormals|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "average_type": [["CUSTOM_NORMAL", "FACE_AREA", "CORNER_ANGLE"]], "weight": ["INT", {"min": 1, "max": 100, "default": 50}], "threshold": ["FLOAT", {"min": 0.0, "max": 10.0, "default": 0.009999999776482582}]}}
|
||||||
Mesh_BeautifyFill|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "angle_limit": ["FLOAT", {"min": 0.0, "max": 3.1415927410125732, "default": 3.1415927410125732}]}}
|
Mesh_BeautifyFill|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "angle_limit": ["FLOAT", {"min": 0.0, "max": 3.1415927410125732, "default": 3.1415927410125732}]}}
|
||||||
@@ -593,5 +607,8 @@ UV_SnapCursor|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "target": [[
|
|||||||
UV_SnapSelected|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "target": [["PIXELS", "CURSOR", "CURSOR_OFFSET", "ADJACENT_UNSELECTED"]]}}
|
UV_SnapSelected|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "target": [["PIXELS", "CURSOR", "CURSOR_OFFSET", "ADJACENT_UNSELECTED"]]}}
|
||||||
UV_SphereProject|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "direction": [["VIEW_ON_EQUATOR", "VIEW_ON_POLES", "ALIGN_TO_OBJECT"]], "align": [["POLAR_ZX", "POLAR_ZY"]], "pole": [["PINCH", "FAN"]], "seam": ["BOOLEAN", {"default": false}], "correct_aspect": ["BOOLEAN", {"default": true}], "clip_to_bounds": ["BOOLEAN", {"default": false}], "scale_to_bounds": ["BOOLEAN", {"default": false}]}}
|
UV_SphereProject|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "direction": [["VIEW_ON_EQUATOR", "VIEW_ON_POLES", "ALIGN_TO_OBJECT"]], "align": [["POLAR_ZX", "POLAR_ZY"]], "pole": [["PINCH", "FAN"]], "seam": ["BOOLEAN", {"default": false}], "correct_aspect": ["BOOLEAN", {"default": true}], "clip_to_bounds": ["BOOLEAN", {"default": false}], "scale_to_bounds": ["BOOLEAN", {"default": false}]}}
|
||||||
UV_Stitch|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "use_limit": ["BOOLEAN", {"default": false}], "snap_islands": ["BOOLEAN", {"default": true}], "limit": ["FLOAT", {"min": 0.0, "max": 3.4028234663852886e+38, "default": 0.009999999776482582}], "static_island": ["INT", {"min": 0, "max": 2147483647, "default": 0}], "active_object_index": ["INT", {"min": 0, "max": 2147483647, "default": 0}], "midpoint_snap": ["BOOLEAN", {"default": false}], "clear_seams": ["BOOLEAN", {"default": true}], "mode": [["VERTEX", "EDGE"]], "stored_mode": [["VERTEX", "EDGE"]], "objects_selection_count": ["B_VECTOR6", {}]}}
|
UV_Stitch|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "use_limit": ["BOOLEAN", {"default": false}], "snap_islands": ["BOOLEAN", {"default": true}], "limit": ["FLOAT", {"min": 0.0, "max": 3.4028234663852886e+38, "default": 0.009999999776482582}], "static_island": ["INT", {"min": 0, "max": 2147483647, "default": 0}], "active_object_index": ["INT", {"min": 0, "max": 2147483647, "default": 0}], "midpoint_snap": ["BOOLEAN", {"default": false}], "clear_seams": ["BOOLEAN", {"default": true}], "mode": [["VERTEX", "EDGE"]], "stored_mode": [["VERTEX", "EDGE"]], "objects_selection_count": ["B_VECTOR6", {}]}}
|
||||||
UV_Unwrap|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "method": [["ANGLE_BASED", "CONFORMAL"]], "fill_holes": ["BOOLEAN", {"default": true}], "correct_aspect": ["BOOLEAN", {"default": true}], "use_subsurf_data": ["BOOLEAN", {"default": false}], "margin_method": [["SCALED", "ADD", "FRACTION"]], "margin": ["FLOAT", {"min": 0.0, "max": 1.0, "default": 0.0010000000474974513}]}}
|
|
||||||
UV_Weld|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"]}}
|
UV_Weld|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"]}}
|
||||||
|
ToGroupOps|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"]}}
|
||||||
|
AvatarMainOutput|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "BPY_OBJS": ["BPY_OBJS"], "open_in_blender": ["BOOLEAN", {"default": false}], "auto_save": ["BOOLEAN", {"default": false}], "blender_path_override": ["STRING", {"multiline": false, "default": ""}], "filename": ["STRING", {"multiline": false, "default": "out"}], "model_type": [["AVA", "GLB", "GLTF_EMBEDDED"]], "write_mode": [["Overwrite", "Increment"]], "SHAPE_FLOW": ["SHAPE_FLOW"]}}
|
||||||
|
GroupOps|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "BPY_OBJ2": ["BPY_OBJ"]}}
|
||||||
|
Object_PlaneTextureUnwrap|{"required": {}, "optional": {"BPY_OBJ": ["BPY_OBJ"], "image": ["IMAGE"], "scale": ["FLOAT", {"default": 1, "display": "number", "step": 0.01}], "texture_name": ["STRING", {"default": "Texture"}]}}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import folder_paths
|
|
||||||
import os
|
|
||||||
from PIL import Image, ImageOps
|
|
||||||
from PIL.PngImagePlugin import PngInfo
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
import json
|
|
||||||
|
|
||||||
class LoadImageWithAlpha:
|
|
||||||
@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": (sorted(files), {"image_upload": True})},
|
|
||||||
}
|
|
||||||
|
|
||||||
CATEGORY = "image"
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "load_image"
|
|
||||||
def load_image(self, image):
|
|
||||||
image_path = folder_paths.get_annotated_filepath(image)
|
|
||||||
i = Image.open(image_path)
|
|
||||||
i = ImageOps.exif_transpose(i)
|
|
||||||
image = i.convert("RGBA")
|
|
||||||
image = np.array(image).astype(np.float32) / 255.0
|
|
||||||
image = torch.from_numpy(image)[None,]
|
|
||||||
|
|
||||||
print(image.shape)
|
|
||||||
|
|
||||||
return (image, )
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def IS_CHANGED(s, image):
|
|
||||||
image_path = folder_paths.get_annotated_filepath(image)
|
|
||||||
m = hashlib.sha256()
|
|
||||||
with open(image_path, 'rb') as f:
|
|
||||||
m.update(f.read())
|
|
||||||
return m.digest().hex()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def VALIDATE_INPUTS(s, image):
|
|
||||||
if not folder_paths.exists_annotated_filepath(image):
|
|
||||||
return "Invalid image file: {}".format(image)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"LoadImageWithAlpha": LoadImageWithAlpha,
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
||||||
"LoadImageWithAlpha": "LoadImageWithAlpha"
|
|
||||||
}
|
|
||||||
+34
-106
@@ -1,9 +1,6 @@
|
|||||||
import atexit
|
import atexit
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import folder_paths
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
def genreate_mesh_from_texture(bpy, image):
|
def genreate_mesh_from_texture(bpy, image):
|
||||||
import torch
|
import torch
|
||||||
@@ -14,12 +11,8 @@ def genreate_mesh_from_texture(bpy, image):
|
|||||||
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
||||||
gray = (gray * 255).astype(np.uint8)
|
gray = (gray * 255).astype(np.uint8)
|
||||||
# Find contours
|
# Find contours
|
||||||
contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
contours, _ = cv2.findContours(
|
||||||
|
gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
if len(contours) == 0:
|
|
||||||
print("Warning: No contours found. The image may have 0 segment.")
|
|
||||||
black_image = torch.zeros(1, *image.shape)
|
|
||||||
return (black_image, None)
|
|
||||||
|
|
||||||
# Get the largest contour
|
# Get the largest contour
|
||||||
areas = [cv2.contourArea(contour) for contour in contours]
|
areas = [cv2.contourArea(contour) for contour in contours]
|
||||||
@@ -39,12 +32,11 @@ def genreate_mesh_from_texture(bpy, image):
|
|||||||
for contour in contours:
|
for contour in contours:
|
||||||
normalized_contour = []
|
normalized_contour = []
|
||||||
for vertex in contour:
|
for vertex in contour:
|
||||||
normalized_vertex = [
|
normalized_vertex = [normalize_vertices(
|
||||||
normalize_vertices(vertex[0][0], width),
|
vertex[0][0], width), normalize_vertices(vertex[0][1], height) * -1]
|
||||||
normalize_vertices(vertex[0][1], height) * -1,
|
|
||||||
]
|
|
||||||
normalized_contour.append(normalized_vertex)
|
normalized_contour.append(normalized_vertex)
|
||||||
normalized_contours.append(np.array(normalized_contour, dtype=np.float32))
|
normalized_contours.append(
|
||||||
|
np.array(normalized_contour, dtype=np.float32))
|
||||||
|
|
||||||
meshes = []
|
meshes = []
|
||||||
# print(len(normalized_contours))
|
# print(len(normalized_contours))
|
||||||
@@ -65,12 +57,12 @@ def genreate_mesh_from_texture(bpy, image):
|
|||||||
mesh.from_pydata(ordered_vertices, [], [face])
|
mesh.from_pydata(ordered_vertices, [], [face])
|
||||||
|
|
||||||
# Create a default shape key for the mesh
|
# Create a default shape key for the mesh
|
||||||
sk_basis = obj.shape_key_add(name="Basis")
|
sk_basis = obj.shape_key_add(name='Basis')
|
||||||
|
|
||||||
meshes.append(obj) # Add the object to the list of meshes
|
meshes.append(obj) # Add the object to the list of meshes
|
||||||
|
|
||||||
# Draw contours on the original image
|
# Draw contours on the original image
|
||||||
if not image.flags["C_CONTIGUOUS"]:
|
if not image.flags['C_CONTIGUOUS']:
|
||||||
image = np.ascontiguousarray(image)
|
image = np.ascontiguousarray(image)
|
||||||
cv2.drawContours(image, contours, -1, (0, 255, 0), 3)
|
cv2.drawContours(image, contours, -1, (0, 255, 0), 3)
|
||||||
|
|
||||||
@@ -82,7 +74,7 @@ def genreate_mesh_from_texture(bpy, image):
|
|||||||
def assign_texture(bpy, BPY_OBJ, texture, texture_name):
|
def assign_texture(bpy, BPY_OBJ, texture, texture_name):
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Start the timer
|
# Start the timer
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
@@ -94,8 +86,7 @@ def assign_texture(bpy, BPY_OBJ, texture, texture_name):
|
|||||||
|
|
||||||
# Create an image with the required dimensions
|
# Create an image with the required dimensions
|
||||||
img = bpy.data.images.new(
|
img = bpy.data.images.new(
|
||||||
texture_name, width=texture.shape[1], height=texture.shape[0], alpha=True
|
texture_name, width=texture.shape[1], height=texture.shape[0])
|
||||||
)
|
|
||||||
|
|
||||||
# If there is no alpha channel, append one full of 1's
|
# If there is no alpha channel, append one full of 1's
|
||||||
if texture.shape[2] == 3:
|
if texture.shape[2] == 3:
|
||||||
@@ -112,7 +103,7 @@ def assign_texture(bpy, BPY_OBJ, texture, texture_name):
|
|||||||
print(f"Time taken (texture.ravel) : {end_time - start_time} seconds")
|
print(f"Time taken (texture.ravel) : {end_time - start_time} seconds")
|
||||||
|
|
||||||
# End the timer and print the time taken
|
# End the timer and print the time taken
|
||||||
|
|
||||||
# Pack image to store it within .blend file
|
# Pack image to store it within .blend file
|
||||||
img.pack()
|
img.pack()
|
||||||
|
|
||||||
@@ -127,27 +118,25 @@ def assign_texture(bpy, BPY_OBJ, texture, texture_name):
|
|||||||
# Create a material
|
# Create a material
|
||||||
mat = bpy.data.materials.new("MaterialName")
|
mat = bpy.data.materials.new("MaterialName")
|
||||||
mat.use_nodes = True
|
mat.use_nodes = True
|
||||||
mat.blend_method = "BLEND"
|
|
||||||
nodes = mat.node_tree.nodes
|
nodes = mat.node_tree.nodes
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
nodes.remove(node)
|
nodes.remove(node)
|
||||||
|
|
||||||
# Add a new texture node
|
# Add a new texture node
|
||||||
texture_node = nodes.new(type="ShaderNodeTexImage")
|
texture_node = nodes.new(type='ShaderNodeTexImage')
|
||||||
texture_node.image = img
|
texture_node.image = img
|
||||||
|
|
||||||
# Add a new BSDF node
|
# Add a new BSDF node
|
||||||
bsdf_node = nodes.new(type="ShaderNodeBsdfPrincipled")
|
bsdf_node = nodes.new(type='ShaderNodeBsdfPrincipled')
|
||||||
|
|
||||||
# Add a new output node
|
# Add a new output node
|
||||||
output_node = nodes.new(type="ShaderNodeOutputMaterial")
|
output_node = nodes.new(type='ShaderNodeOutputMaterial')
|
||||||
|
|
||||||
# Link nodes together
|
# Link nodes together
|
||||||
links = mat.node_tree.links
|
links = mat.node_tree.links
|
||||||
links.new(bsdf_node.inputs["Base Color"], texture_node.outputs["Color"])
|
links.new(bsdf_node.inputs['Base Color'],
|
||||||
links.new(output_node.inputs["Surface"], bsdf_node.outputs["BSDF"])
|
texture_node.outputs['Color'])
|
||||||
|
links.new(output_node.inputs['Surface'], bsdf_node.outputs['BSDF'])
|
||||||
links.new(bsdf_node.inputs["Alpha"], texture_node.outputs["Alpha"])
|
|
||||||
|
|
||||||
# Assign the material to the active object
|
# Assign the material to the active object
|
||||||
if obj.data.materials:
|
if obj.data.materials:
|
||||||
@@ -162,30 +151,21 @@ def assign_texture(bpy, BPY_OBJ, texture, texture_name):
|
|||||||
blender_process_global = []
|
blender_process_global = []
|
||||||
|
|
||||||
|
|
||||||
def open_in_blender(
|
def open_in_blender(blender_process, blender_path, output_file, camera_location=(0, 0, 0), camera_rotation=(0, 0, 0), shading="Material"):
|
||||||
blender_process,
|
|
||||||
blender_path,
|
|
||||||
output_file,
|
|
||||||
camera_location=(0, 0, 0),
|
|
||||||
camera_rotation=(0, 0, 0),
|
|
||||||
shading="Material",
|
|
||||||
):
|
|
||||||
import global_bpy
|
import global_bpy
|
||||||
import mathutils
|
import mathutils
|
||||||
|
|
||||||
bpy = global_bpy.get_bpy()
|
bpy = global_bpy.get_bpy()
|
||||||
|
|
||||||
# Change shading mode and viewport
|
# Change shading mode and viewport
|
||||||
for area in bpy.context.screen.areas:
|
for area in bpy.context.screen.areas:
|
||||||
if area.type == "VIEW_3D":
|
if area.type == 'VIEW_3D':
|
||||||
for space in area.spaces:
|
for space in area.spaces:
|
||||||
if space.type == "VIEW_3D":
|
if space.type == 'VIEW_3D':
|
||||||
space.shading.type = shading.upper()
|
space.shading.type = shading.upper()
|
||||||
rv3d = space.region_3d
|
rv3d = space.region_3d
|
||||||
rv3d.view_location = camera_location
|
rv3d.view_location = camera_location
|
||||||
rv3d.view_rotation = mathutils.Euler(
|
rv3d.view_rotation = mathutils.Euler(
|
||||||
camera_rotation
|
camera_rotation).to_quaternion()
|
||||||
).to_quaternion()
|
|
||||||
|
|
||||||
# Open blender
|
# Open blender
|
||||||
if blender_process != None:
|
if blender_process != None:
|
||||||
@@ -198,8 +178,8 @@ def open_in_blender(
|
|||||||
os.remove(output_file)
|
os.remove(output_file)
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=output_file)
|
bpy.ops.wm.save_as_mainfile(filepath=output_file)
|
||||||
|
|
||||||
print("blender_path", blender_path)
|
print('blender_path', blender_path)
|
||||||
print("output_file", output_file)
|
print('output_file', output_file)
|
||||||
blender_process = subprocess.Popen([blender_path, output_file])
|
blender_process = subprocess.Popen([blender_path, output_file])
|
||||||
# append to global list so it doesn't get garbage collected
|
# append to global list so it doesn't get garbage collected
|
||||||
blender_process_global.append(blender_process)
|
blender_process_global.append(blender_process)
|
||||||
@@ -209,20 +189,18 @@ def open_in_blender(
|
|||||||
|
|
||||||
# detects when the python process is killed, and kills the blender process
|
# detects when the python process is killed, and kills the blender process
|
||||||
|
|
||||||
|
|
||||||
@atexit.register
|
@atexit.register
|
||||||
def kill_blender_process():
|
def kill_blender_process():
|
||||||
print("blender_process_global", blender_process_global)
|
print('blender_process_global', blender_process_global)
|
||||||
for process in blender_process_global:
|
for process in blender_process_global:
|
||||||
process.kill()
|
process.kill()
|
||||||
|
|
||||||
|
|
||||||
def export_gltf(output_dir, bpy_objects, filename, model_type, write_mode, metadata):
|
def export_gltf(output_dir, bpy_objects, filename, model_type, write_mode, metadata):
|
||||||
import global_bpy
|
import global_bpy
|
||||||
|
|
||||||
bpy = global_bpy.get_bpy()
|
bpy = global_bpy.get_bpy()
|
||||||
# print(bpy, bpy_objects)
|
# print(bpy, bpy_objects)
|
||||||
|
|
||||||
# deselect all objects
|
# deselect all objects
|
||||||
override = bpy.context.copy()
|
override = bpy.context.copy()
|
||||||
override["selected_objects"] = list(bpy_objects)
|
override["selected_objects"] = list(bpy_objects)
|
||||||
@@ -243,72 +221,22 @@ def export_gltf(output_dir, bpy_objects, filename, model_type, write_mode, metad
|
|||||||
return ".ava"
|
return ".ava"
|
||||||
|
|
||||||
ext = get_file_extension(model_type)
|
ext = get_file_extension(model_type)
|
||||||
filepath = (
|
filepath = output_dir + "/" + filename + ext + (".glb" if model_type == "AVA" else "")
|
||||||
output_dir + "/" + filename + ext + (".glb" if model_type == "AVA" else "")
|
|
||||||
)
|
|
||||||
|
|
||||||
if write_mode == "Increment":
|
if write_mode == "Increment":
|
||||||
count = 0
|
count = 0
|
||||||
# while file exists, increment count
|
# while file exists, increment count
|
||||||
while os.path.exists(output_dir + "/" + filename + "_" + str(count) + ext):
|
while os.path.exists(output_dir + "/" + filename + '_' + str(count) + ext):
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
filepath = (
|
filepath = output_dir + "/" + filename + '_' + str(count) + ext + (".glb" if model_type == "AVA" else "")
|
||||||
output_dir
|
|
||||||
+ "/"
|
|
||||||
+ filename
|
|
||||||
+ "_"
|
|
||||||
+ str(count)
|
|
||||||
+ ext
|
|
||||||
+ (".glb" if model_type == "AVA" else "")
|
|
||||||
)
|
|
||||||
|
|
||||||
with bpy.context.temp_override(**override):
|
with bpy.context.temp_override(**override):
|
||||||
bpy.ops.export_scene.gltf(
|
bpy.ops.export_scene.gltf(filepath=filepath, export_format="GLB" if model_type == "AVA" else model_type, use_selection=True, export_extras=True)
|
||||||
filepath=filepath,
|
|
||||||
export_format="GLB" if model_type == "AVA" else model_type,
|
|
||||||
use_selection=True,
|
|
||||||
export_extras=True,
|
|
||||||
)
|
|
||||||
# print(filepath)
|
# print(filepath)
|
||||||
if filepath.endswith(".ava.glb"):
|
if filepath.endswith('.ava.glb'):
|
||||||
new_filepath = filepath.replace(".ava.glb", ".ava")
|
new_filepath = filepath.replace('.ava.glb', '.ava')
|
||||||
os.replace(filepath, new_filepath)
|
os.rename(filepath, new_filepath)
|
||||||
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
|
|
||||||
@@ -24,8 +24,6 @@ class Object_CreateMeshLayer(blender_node.ObjectOps):
|
|||||||
|
|
||||||
def blender_process(self, bpy, image, convex_hull, shape_threshold, mesh_layer_name, scale_x,scale_y , extrude_x, extrude_y, seed):
|
def blender_process(self, bpy, image, convex_hull, shape_threshold, mesh_layer_name, scale_x,scale_y , extrude_x, extrude_y, seed):
|
||||||
image, BPY_OBJ = genreate_mesh_from_texture(bpy, image)
|
image, BPY_OBJ = genreate_mesh_from_texture(bpy, image)
|
||||||
if BPY_OBJ is None:
|
|
||||||
return (None, image)
|
|
||||||
|
|
||||||
bpy.context.view_layer.objects.active = BPY_OBJ
|
bpy.context.view_layer.objects.active = BPY_OBJ
|
||||||
|
|
||||||
@@ -43,7 +41,7 @@ class Object_CreateMeshLayer(blender_node.ObjectOps):
|
|||||||
bpy.ops.mesh.select_all(action='SELECT')
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
bpy.ops.mesh.edge_face_add()
|
bpy.ops.mesh.edge_face_add()
|
||||||
|
|
||||||
bpy.ops.transform.resize(value=(float(scale_x), float(scale_y), 1))
|
bpy.ops.transform.resize(value=(scale_x, scale_y, 1))
|
||||||
|
|
||||||
bpy.context.object.vertex_groups.new(name=mesh_layer_name)
|
bpy.context.object.vertex_groups.new(name=mesh_layer_name)
|
||||||
bpy.ops.object.vertex_group_assign()
|
bpy.ops.object.vertex_group_assign()
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import blender_node
|
|
||||||
from mesh_utils import genreate_mesh_from_texture
|
|
||||||
|
|
||||||
class Object_CreateMeshLayer_Advanced(blender_node.ObjectOps):
|
|
||||||
|
|
||||||
BASE_INPUT_TYPES = {}
|
|
||||||
|
|
||||||
CUSTOM_NAME = "Create Mesh Layer (Advanced)"
|
|
||||||
|
|
||||||
EXTRA_INPUT_TYPES = {
|
|
||||||
"image": ("IMAGE",),
|
|
||||||
"convex_hull": ("BOOLEAN", {"default": True}),
|
|
||||||
# "face_threshold": ("FLOAT", {"display": "number", "default": 0.7}),
|
|
||||||
"shape_threshold": ("FLOAT", {"display": "number", "default": 0.7}),
|
|
||||||
"mesh_layer_name": ("STRING", {"default": "mesh_layer"}),
|
|
||||||
"scale_x": ("FLOAT", {"display": "number", "default": 1}),
|
|
||||||
"scale_y": ("FLOAT", {"display": "number", "default": 1}),
|
|
||||||
"extrude_x": ("FLOAT", {"display": "number", "default": 0}),
|
|
||||||
"extrude_y": ("FLOAT", {"display": "number", "default": 0}),
|
|
||||||
"inner_translate_x": ("FLOAT", {"display": "number", "default": 0, "step": 0.01}),
|
|
||||||
"inner_translate_y": ("FLOAT", {"display": "number", "default": 0, "step": 0.01}),
|
|
||||||
"outer_translate_x": ("FLOAT", {"display": "number", "default": 0, "step": 0.01}),
|
|
||||||
"outer_translate_y": ("FLOAT", {"display": "number", "default": 0, "step": 0.01}),
|
|
||||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("BPY_OBJ", "IMAGE")
|
|
||||||
|
|
||||||
def blender_process(self, bpy, image, convex_hull, shape_threshold, mesh_layer_name, scale_x,scale_y , extrude_x, extrude_y, inner_translate_x, inner_translate_y, outer_translate_x, outer_translate_y, seed):
|
|
||||||
image, BPY_OBJ = genreate_mesh_from_texture(bpy, image)
|
|
||||||
if BPY_OBJ is None:
|
|
||||||
return (None, image)
|
|
||||||
|
|
||||||
bpy.context.view_layer.objects.active = BPY_OBJ
|
|
||||||
|
|
||||||
self.edit_mode(bpy)
|
|
||||||
|
|
||||||
if convex_hull:
|
|
||||||
bpy.ops.mesh.convex_hull(
|
|
||||||
delete_unused=True, use_existing_faces=True,
|
|
||||||
shape_threshold=shape_threshold,
|
|
||||||
# face_threshold=face_threshold
|
|
||||||
face_threshold=0.7
|
|
||||||
)
|
|
||||||
|
|
||||||
bpy.ops.mesh.delete(type='EDGE_FACE')
|
|
||||||
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.translate(value=(inner_translate_x, inner_translate_y, 0))
|
|
||||||
|
|
||||||
bpy.context.object.vertex_groups.new(name=mesh_layer_name)
|
|
||||||
bpy.ops.object.vertex_group_assign()
|
|
||||||
|
|
||||||
if extrude_x != 0 or extrude_y != 0:
|
|
||||||
bpy.ops.mesh.extrude_region_move()
|
|
||||||
bpy.ops.object.vertex_group_remove_from()
|
|
||||||
bpy.ops.transform.resize(value=(extrude_x, extrude_y, 0))
|
|
||||||
bpy.ops.transform.translate(value=(outer_translate_x, outer_translate_y, 0))
|
|
||||||
bpy.ops.mesh.delete(type='ONLY_FACE')
|
|
||||||
|
|
||||||
self.object_mode(bpy)
|
|
||||||
return (BPY_OBJ, image)
|
|
||||||
@@ -8,6 +8,5 @@ class GroupOps(blender_node.ObjectOps):
|
|||||||
RETURN_TYPES = (blender_node.BPY_OBJS,)
|
RETURN_TYPES = (blender_node.BPY_OBJS,)
|
||||||
|
|
||||||
def blender_process(self, bpy, BPY_OBJ, BPY_OBJ2, **props):
|
def blender_process(self, bpy, BPY_OBJ, BPY_OBJ2, **props):
|
||||||
prop_values = props.values()
|
return ([BPY_OBJ, BPY_OBJ2],)
|
||||||
return ([BPY_OBJ, BPY_OBJ2, *prop_values],)
|
|
||||||
|
|
||||||
|
|||||||
+8
-7
@@ -2,17 +2,18 @@ import blender_node
|
|||||||
|
|
||||||
|
|
||||||
class Mesh_JoinMesh(blender_node.ObjectOps):
|
class Mesh_JoinMesh(blender_node.ObjectOps):
|
||||||
EXTRA_INPUT_TYPES = {"BPY_OBJ2": (blender_node.BPY_OBJ,)}
|
EXTRA_INPUT_TYPES = {
|
||||||
|
"BPY_OBJ2": (blender_node.BPY_OBJ,)
|
||||||
|
}
|
||||||
|
|
||||||
CUSTOM_NAME = "Join Meshes"
|
CUSTOM_NAME = "Join Meshes"
|
||||||
|
|
||||||
def blender_process(self, bpy, BPY_OBJ, **props):
|
def blender_process(self, bpy, BPY_OBJ, **props):
|
||||||
prop_values = props.values()
|
prop_values = props.values()
|
||||||
for obj in list(prop_values) + [BPY_OBJ]:
|
for obj in list(prop_values) + [BPY_OBJ]:
|
||||||
if obj is not None:
|
obj.select_set(True)
|
||||||
obj.select_set(True)
|
bpy.context.view_layer.objects.active = BPY_OBJ
|
||||||
if bpy.context.view_layer.objects is not None:
|
|
||||||
bpy.context.view_layer.objects.active = BPY_OBJ
|
|
||||||
bpy.ops.object.join()
|
bpy.ops.object.join()
|
||||||
|
|
||||||
return (BPY_OBJ,)
|
return (BPY_OBJ,)
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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,)
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import folder_paths
|
|
||||||
import os
|
|
||||||
from PIL import Image, ImageOps
|
|
||||||
from PIL.PngImagePlugin import PngInfo
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
import json
|
|
||||||
|
|
||||||
class SaveImageWithWorkflow:
|
|
||||||
def __init__(self):
|
|
||||||
self.output_dir = folder_paths.get_output_directory()
|
|
||||||
self.type = "output"
|
|
||||||
self.prefix_append = ""
|
|
||||||
|
|
||||||
@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": (sorted(files), {"image_upload": True}),
|
|
||||||
"filename_prefix": ("STRING", {"default": "ComfyUI"})},
|
|
||||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ()
|
|
||||||
FUNCTION = "save_images"
|
|
||||||
|
|
||||||
OUTPUT_NODE = True
|
|
||||||
|
|
||||||
CATEGORY = "image"
|
|
||||||
|
|
||||||
def save_images(self, image, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
|
|
||||||
image_path = folder_paths.get_annotated_filepath(image)
|
|
||||||
i = Image.open(image_path)
|
|
||||||
i = ImageOps.exif_transpose(i)
|
|
||||||
image = i.convert("RGBA")
|
|
||||||
image = np.array(image).astype(np.float32) / 255.0
|
|
||||||
image = torch.from_numpy(image)[None,]
|
|
||||||
images=(image)
|
|
||||||
|
|
||||||
filename_prefix += self.prefix_append
|
|
||||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
|
||||||
results = list()
|
|
||||||
for image in images:
|
|
||||||
i = 255. * image.cpu().numpy()
|
|
||||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
||||||
metadata = None
|
|
||||||
metadata = PngInfo()
|
|
||||||
if prompt is not None:
|
|
||||||
# prompt = json.loads(prompt)
|
|
||||||
prompt = {k: v for k, v in prompt.items() if v['class_type'] != 'Save Image With Workflow'}
|
|
||||||
metadata.add_text("prompt", json.dumps(prompt))
|
|
||||||
|
|
||||||
print(extra_pnginfo)
|
|
||||||
|
|
||||||
# if prompt is not None:
|
|
||||||
# metadata.add_text("prompt", json.dumps(prompt))
|
|
||||||
if extra_pnginfo is not None:
|
|
||||||
for x in extra_pnginfo:
|
|
||||||
if (x == 'workflow'):
|
|
||||||
extra_pnginfo[x]["nodes"] = [node for node in extra_pnginfo[x]["nodes"] if node['type'] != 'Save Image With Workflow']
|
|
||||||
|
|
||||||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
|
||||||
|
|
||||||
file = f"{filename}_{counter:05}_.png"
|
|
||||||
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4)
|
|
||||||
results.append({
|
|
||||||
"filename": file,
|
|
||||||
"subfolder": subfolder,
|
|
||||||
"type": self.type
|
|
||||||
})
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
return { "ui": { "images": results } }
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"Save Image With Workflow": SaveImageWithWorkflow,
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
||||||
"Save Image With Workflow": "Save Image With Workflow"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
class UV_Unwrap():
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"bpy_objs_target": ("BPY_OBJS",),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("BPY_OBJS",)
|
||||||
|
RETURN_NAMES = ("bpy_objs",)
|
||||||
|
|
||||||
|
FUNCTION = "process"
|
||||||
|
|
||||||
|
CATEGORY = "mesh"
|
||||||
|
|
||||||
|
def process(self, bpy_objs_target):
|
||||||
|
import global_bpy
|
||||||
|
bpy = global_bpy.get_bpy()
|
||||||
|
|
||||||
|
target_object = bpy_objs_target[0]
|
||||||
|
|
||||||
|
bpy.context.view_layer.objects.active = target_object
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
# deselect all objects
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
# select only the target object
|
||||||
|
bpy.context.view_layer.objects.active = target_object
|
||||||
|
# enter enter edit mode and select all faces of the object to fill
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
|
||||||
|
# perform a cube projection unwrap
|
||||||
|
bpy.ops.uv.cube_project(cube_size=1.0, correct_aspect=True, clip_to_bounds=False, scale_to_bounds=True)
|
||||||
|
|
||||||
|
# bpy.ops.mesh.delete(type='FACE')
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
|
||||||
|
return ([target_object],)
|
||||||
|
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"UV_Unwrap": UV_Unwrap
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"UV_Unwrap": "UV Unwrap"
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
os.environ["TYPE_GENERATION"] = "1"
|
|
||||||
|
|
||||||
current_dir = os.path.dirname(__file__)
|
|
||||||
blender_dir = os.path.join(current_dir, "blender")
|
|
||||||
sys.path.append(blender_dir)
|
|
||||||
|
|
||||||
import json
|
|
||||||
from blender.ops_mesh import BLENDER_NODES
|
|
||||||
|
|
||||||
|
|
||||||
with open(f"{blender_dir}/input_types.txt", "w") as f:
|
|
||||||
for node in BLENDER_NODES:
|
|
||||||
results = node.INPUT_TYPES()
|
|
||||||
f.write(node.__name__ + "|" + json.dumps(results) + "\n")
|
|
||||||
+3
-19
@@ -3,25 +3,8 @@ import { van } from "./van.js";
|
|||||||
const { div, span } = van.tags;
|
const { div, span } = van.tags;
|
||||||
|
|
||||||
export function Alert() {
|
export function Alert() {
|
||||||
const color = van.state("bg-orange-100 text-orange-700 border-orange-500");
|
|
||||||
|
|
||||||
van.derive(() => {
|
van.derive(() => {
|
||||||
if (alertDialog.val.time > 0) {
|
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(() => {
|
setTimeout(() => {
|
||||||
alertDialog.val = { text: "", time: 0 };
|
alertDialog.val = { text: "", time: 0 };
|
||||||
}, alertDialog.val.time);
|
}, alertDialog.val.time);
|
||||||
@@ -31,12 +14,13 @@ export function Alert() {
|
|||||||
return div(
|
return div(
|
||||||
{
|
{
|
||||||
class: () =>
|
class: () =>
|
||||||
"absolute z-[100] bottom-8 flex justify-center w-full " +
|
"absolute bottom-8 flex justify-center w-full " +
|
||||||
(alertDialog.val.text ? "" : "hidden"),
|
(alertDialog.val.text ? "" : "hidden"),
|
||||||
},
|
},
|
||||||
div(
|
div(
|
||||||
{
|
{
|
||||||
class: () => `${color.val} border-t-4 rounded-sm p-2`,
|
class:
|
||||||
|
"bg-orange-100 border-t-4 border-orange-500 rounded-sm text-orange-700 p-2",
|
||||||
},
|
},
|
||||||
() => span(alertDialog.val.text)
|
() => span(alertDialog.val.text)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+12
-761
@@ -1,774 +1,25 @@
|
|||||||
import { van } from "./van.js";
|
import { van } from "./van.js";
|
||||||
import {
|
const { button, iframe, div, img } = van.tags;
|
||||||
imageUrl,
|
import { showEditor, previewUrl } from "./state.js";
|
||||||
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() {
|
export function AvatarPreview() {
|
||||||
console.log("getting workflow json now");
|
return div(
|
||||||
|
{
|
||||||
const loading = van.state(false);
|
class: () =>
|
||||||
const shareLoading = van.state("share"); // share, loading, shared
|
"w-[320px] h-[370px] absolute right-0 top-0 z-[100] pointer-events-auto mt-4 mr-4 " +
|
||||||
|
(!showEditor.val ? "" : "hidden"),
|
||||||
api.addEventListener("execution_start", (evt) => {
|
},
|
||||||
loading.val = true;
|
iframe({
|
||||||
});
|
|
||||||
|
|
||||||
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",
|
id: "avatech-viewer-iframe",
|
||||||
title: "avatech-viewer-iframe",
|
title: "avatech-viewer-iframe",
|
||||||
name: "avatech-viewer-iframe",
|
name: "avatech-viewer-iframe",
|
||||||
allow: "cross-origin-isolated",
|
allow: "cross-origin-isolated",
|
||||||
class: () =>
|
class: () =>
|
||||||
"w-full h-full min-w-[350px] min-h-[350px] z-[100] pointer-events-auto flex border-none overflow-hidden bg-transparent" +
|
"w-full h-full flex pointer-events-auto rounded-2xl border-none " +
|
||||||
(showPreview.val ? "" : "hidden"),
|
(!showEditor.val ? "" : "hidden"),
|
||||||
// src: "https://labs.avatech.ai/viewer/default",
|
// src: "https://labs.avatech.ai/viewer/default",
|
||||||
// src: "http://localhost:3000/viewer/default",
|
// src: "http://localhost:3000/viewer/default",
|
||||||
src: previewUrl,
|
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 && !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"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
renderIFrame(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+9
-12
@@ -1,24 +1,21 @@
|
|||||||
import { LayerEditor } from "./LayerEditor.js";
|
import { LayerEditor } from './LayerEditor.js';
|
||||||
import { ShapeFlowEditor } from "./ShapeFlowEditor.js";
|
import { ShapeFlowEditor } from './ShapeFlowEditor.js';
|
||||||
import { van } from "./van.js";
|
import { van } from './van.js';
|
||||||
import { AvatarPreview } from "./AvatarPreview.js";
|
import { AvatarPreview } from './AvatarPreview.js';
|
||||||
import { Loading } from "./Loading.js";
|
import { Loading } from './Loading.js';
|
||||||
import { Alert } from "./Alert.js";
|
import { Alert } from './Alert.js';
|
||||||
import { AppHeader } from "./AppHeader.js";
|
|
||||||
import { CombinePointsDialog } from "./CombinePointsDialog.js";
|
|
||||||
const { button, iframe, div, img } = van.tags;
|
const { button, iframe, div, img } = van.tags;
|
||||||
|
|
||||||
export function Container() {
|
export function Container() {
|
||||||
return div(
|
return div(
|
||||||
{
|
{
|
||||||
class: "fixed left-0 top-0 w-full h-full z-[1000] pointer-events-none",
|
class: 'fixed left-0 top-0 w-full h-full z-[1000] pointer-events-none',
|
||||||
id: "avatech-editor",
|
id: 'avatech-editor',
|
||||||
},
|
},
|
||||||
CombinePointsDialog(),
|
|
||||||
ShapeFlowEditor(),
|
ShapeFlowEditor(),
|
||||||
LayerEditor(),
|
LayerEditor(),
|
||||||
AvatarPreview(),
|
AvatarPreview(),
|
||||||
Loading(),
|
Loading(),
|
||||||
Alert()
|
Alert(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+52
-483
@@ -1,7 +1,5 @@
|
|||||||
import { SideBar } from "./SideBar.js";
|
import { SideBar } from "./SideBar.js";
|
||||||
import { api } from "./api.js";
|
import { initModel, runONNX } from "./onnx.js";
|
||||||
import { app } from "./app.js";
|
|
||||||
import { runONNX } from "./onnx.js";
|
|
||||||
import {
|
import {
|
||||||
showImageEditor,
|
showImageEditor,
|
||||||
point_label,
|
point_label,
|
||||||
@@ -13,295 +11,11 @@ import {
|
|||||||
selectedLayer,
|
selectedLayer,
|
||||||
imagePromptsMulti,
|
imagePromptsMulti,
|
||||||
embeddings,
|
embeddings,
|
||||||
embeddingID,
|
|
||||||
alertDialog,
|
|
||||||
allImagePrompts,
|
|
||||||
boxesMulti,
|
|
||||||
enableAutoSegment,
|
|
||||||
} from "./state.js";
|
} from "./state.js";
|
||||||
import { van } from "./van.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;
|
const { button, div, img, canvas, span } = van.tags;
|
||||||
|
|
||||||
let throttle = false;
|
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() {
|
export function updateImagePrompts() {
|
||||||
if (selectedLayer.val !== "" && selectedLayer.val !== undefined) {
|
if (selectedLayer.val !== "" && selectedLayer.val !== undefined) {
|
||||||
@@ -312,18 +26,6 @@ export function updateImagePrompts() {
|
|||||||
|
|
||||||
targetNode.val.widgets.find((x) => x.name === "image_prompts_json").value =
|
targetNode.val.widgets.find((x) => x.name === "image_prompts_json").value =
|
||||||
JSON.stringify(imagePromptsMulti.val);
|
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,
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// });
|
|
||||||
} else {
|
} else {
|
||||||
targetNode.val.widgets.find((x) => x.name === "image_prompts_json").value =
|
targetNode.val.widgets.find((x) => x.name === "image_prompts_json").value =
|
||||||
JSON.stringify(imagePrompts.val);
|
JSON.stringify(imagePrompts.val);
|
||||||
@@ -331,44 +33,7 @@ export function updateImagePrompts() {
|
|||||||
targetNode.val.graph.change();
|
targetNode.val.graph.change();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadSegments() {
|
function handleClick(e) {
|
||||||
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 rect = e.target.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
const y = e.clientY - rect.top;
|
const y = e.clientY - rect.top;
|
||||||
@@ -380,28 +45,22 @@ async function handleClick(e) {
|
|||||||
imageSize.val.imgScale
|
imageSize.val.imgScale
|
||||||
);
|
);
|
||||||
|
|
||||||
let label;
|
|
||||||
if (isMobileDevice()) {
|
|
||||||
label = positivePrompt.val ? 1 : 0;
|
|
||||||
} else {
|
|
||||||
label = e.isRight ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
imagePrompts.val = [
|
imagePrompts.val = [
|
||||||
...imagePrompts.val,
|
...imagePrompts.val,
|
||||||
{ x: relativeX, y: relativeY, label },
|
{ x: relativeX, y: relativeY, label: e.isRight ? 0 : 1 },
|
||||||
];
|
];
|
||||||
await drawSegment(getClicks());
|
|
||||||
updateImagePrompts();
|
updateImagePrompts();
|
||||||
|
drawSegment(getClicks());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePointClick(e, point) {
|
function handlePointClick(e, point) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
imagePrompts.val = imagePrompts.val.filter(
|
imagePrompts.val = imagePrompts.val.filter(
|
||||||
(x) => !(x.x === point.x && x.y === point.y)
|
(x) => !(x.x === point.x && x.y === point.y)
|
||||||
);
|
);
|
||||||
await drawSegment(getClicks());
|
|
||||||
updateImagePrompts();
|
updateImagePrompts();
|
||||||
|
drawSegment(getClicks());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleImageSize(image) {
|
function handleImageSize(image) {
|
||||||
@@ -415,16 +74,15 @@ function handleImageSize(image) {
|
|||||||
return { height: h, width: w, samScale, imgScale };
|
return { height: h, width: w, samScale, imgScale };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClicks(prompts) {
|
export function getClicks() {
|
||||||
return (prompts || imagePrompts.val).map((point) => ({
|
return imagePrompts.val.map((point) => ({
|
||||||
x: point.x,
|
x: point.x,
|
||||||
y: point.y,
|
y: point.y,
|
||||||
clickType: point.label,
|
clickType: point.label,
|
||||||
isAuto: point.isAuto,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function drawSegment(clicks, layer, drawBox = true) {
|
export function drawSegment(clicks) {
|
||||||
const canvas = document.getElementById("mask-canvas");
|
const canvas = document.getElementById("mask-canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (clicks.length === 0) {
|
if (clicks.length === 0) {
|
||||||
@@ -432,34 +90,19 @@ export async function drawSegment(clicks, layer, drawBox = true) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (embeddings.val) {
|
if (embeddings.val) {
|
||||||
const box = enableAutoSegment.val
|
runONNX(clicks, embeddings.val).then((mask) => {
|
||||||
? boxesMulti.val[layer || selectedLayer.val]
|
if (mask) {
|
||||||
: null;
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
const filteredClicks = enableAutoSegment.val
|
ctx.drawImage(mask, 0, 0);
|
||||||
? 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);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initModel();
|
||||||
|
|
||||||
export function LayerEditor() {
|
export function LayerEditor() {
|
||||||
let realTimeSegment = true;
|
let realTimeSegment = true;
|
||||||
|
|
||||||
const showSidebar = van.state(true);
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (showImageEditor.val && e.code === "Tab") {
|
if (showImageEditor.val && e.code === "Tab") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -473,97 +116,29 @@ export function LayerEditor() {
|
|||||||
return div(
|
return div(
|
||||||
{
|
{
|
||||||
class: () =>
|
class: () =>
|
||||||
"absolute flex bg-gray-900 bg-opacity-50 top-0 w-full h-full pointer-events-auto z-[1000] " +
|
"absolute flex bg-gray-900 bg-opacity-50 top-0 w-full h-full pointer-events-auto " +
|
||||||
(showImageEditor.val ? "" : "hidden"),
|
(showImageEditor.val ? "" : "hidden"),
|
||||||
},
|
},
|
||||||
div(
|
button(
|
||||||
{
|
{
|
||||||
class:
|
class: () =>
|
||||||
"absolute top-4 left-4 right-0 flex w-full gap-2 justify-start z-[200]",
|
"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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
button(
|
span({
|
||||||
{
|
class: "iconify text-lg",
|
||||||
class: () => "btn btn-neutral flex flex-row normal-case rounded-md",
|
"data-icon": "ic:baseline-arrow-back",
|
||||||
onclick: async () => {
|
"data-inline": "false",
|
||||||
console.log("close");
|
}),
|
||||||
showImageEditor.val = false;
|
div("Back")
|
||||||
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(
|
div(
|
||||||
{
|
{
|
||||||
class:
|
class:
|
||||||
"hidden w-full justify-center absolute top-0 left-0 right-0 items-center",
|
"hidden w-full flex justify-center absolute top-0 left-0 right-0 items-center",
|
||||||
},
|
},
|
||||||
button(
|
button(
|
||||||
{
|
{
|
||||||
@@ -590,11 +165,10 @@ export function LayerEditor() {
|
|||||||
id: "image-container",
|
id: "image-container",
|
||||||
},
|
},
|
||||||
img({
|
img({
|
||||||
id: "image",
|
|
||||||
class:
|
class:
|
||||||
"fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2",
|
"fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2",
|
||||||
src: imageUrl,
|
src: imageUrl,
|
||||||
onload: async (e) => {
|
onload: (e) => {
|
||||||
imageSize.val = handleImageSize(e.target);
|
imageSize.val = handleImageSize(e.target);
|
||||||
|
|
||||||
document.getElementById("image-container").style.scale =
|
document.getElementById("image-container").style.scale =
|
||||||
@@ -609,13 +183,13 @@ export function LayerEditor() {
|
|||||||
canvas.width = e.target.naturalWidth;
|
canvas.width = e.target.naturalWidth;
|
||||||
canvas.height = e.target.naturalHeight;
|
canvas.height = e.target.naturalHeight;
|
||||||
},
|
},
|
||||||
oncontextmenu: async (e) => {
|
oncontextmenu: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.isRight = true;
|
e.isRight = true;
|
||||||
await handleClick(e);
|
handleClick(e);
|
||||||
},
|
},
|
||||||
onclick: async (e) => {
|
onclick: (e) => {
|
||||||
await handleClick(e);
|
handleClick(e);
|
||||||
},
|
},
|
||||||
onmouseleave: (e) => {
|
onmouseleave: (e) => {
|
||||||
drawSegment(getClicks());
|
drawSegment(getClicks());
|
||||||
@@ -649,25 +223,19 @@ export function LayerEditor() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
() =>
|
canvas({
|
||||||
canvas({
|
class:
|
||||||
class:
|
"pointer-events-none fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-80",
|
||||||
"pointer-events-none fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-80",
|
id: "mask-canvas",
|
||||||
style: () =>
|
}),
|
||||||
`width: ${imageContainerSize.val.width}px; height: ${imageContainerSize.val.height}px;`,
|
() => {
|
||||||
id: "mask-canvas",
|
return div(
|
||||||
}),
|
|
||||||
() =>
|
|
||||||
div(
|
|
||||||
{
|
{
|
||||||
class: "absolute w-full h-full pointer-events-none",
|
class: "absolute w-full h-full pointer-events-none",
|
||||||
style: () =>
|
style: () =>
|
||||||
`width: ${imageContainerSize.val.width}px; height: ${imageContainerSize.val.height}px;`,
|
`width: ${imageContainerSize.val.width}px; height: ${imageContainerSize.val.height}px;`,
|
||||||
},
|
},
|
||||||
...(enableAutoSegment.val
|
...imagePrompts.val?.map((point) => {
|
||||||
? imagePrompts.val
|
|
||||||
: imagePrompts.val?.filter((click) => !click.isAuto)
|
|
||||||
).map((point) => {
|
|
||||||
return button({
|
return button({
|
||||||
style: () =>
|
style: () =>
|
||||||
`left: ${
|
`left: ${
|
||||||
@@ -681,16 +249,17 @@ export function LayerEditor() {
|
|||||||
point.label === 1 ? "bg-green-500" : "bg-red-500"
|
point.label === 1 ? "bg-green-500" : "bg-red-500"
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
oncontextmenu: async (e) => {
|
oncontextmenu: (e) => {
|
||||||
await handlePointClick(e, point);
|
handlePointClick(e, point);
|
||||||
},
|
},
|
||||||
onclick: async (e) => {
|
onclick: (e) => {
|
||||||
await handlePointClick(e, point);
|
handlePointClick(e, point);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
() => (showSidebar.val ? SideBar() : div())
|
SideBar()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export function Loading() {
|
|||||||
return div(
|
return div(
|
||||||
{
|
{
|
||||||
class: () =>
|
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 z-[1001] " +
|
"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 " +
|
||||||
(showLoading.val ? "" : "hidden"),
|
(showLoading.val ? "" : "hidden"),
|
||||||
},
|
},
|
||||||
span({
|
span({
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function ShapeFlowEditor() {
|
|||||||
{
|
{
|
||||||
class: "modal-box",
|
class: "modal-box",
|
||||||
},
|
},
|
||||||
div({ class: "text-black" }, div({ class: "text-xl font-bold" }, "Shape Flow Editor"), div({ class: "" }, "The shape flow will be save in CreateShapeFlow node (comfyui node)! Or discard the changes!")),
|
div({ class: "text-black" }, "This is a dialog"),
|
||||||
div(
|
div(
|
||||||
{ class: "modal-action" },
|
{ class: "modal-action" },
|
||||||
form(
|
form(
|
||||||
|
|||||||
+193
-247
@@ -5,8 +5,6 @@ import {
|
|||||||
imagePromptsMulti,
|
imagePromptsMulti,
|
||||||
targetNode,
|
targetNode,
|
||||||
showImageEditor,
|
showImageEditor,
|
||||||
allImagePrompts,
|
|
||||||
samPrompts,
|
|
||||||
} from "./state.js";
|
} from "./state.js";
|
||||||
import { van } from "./van.js";
|
import { van } from "./van.js";
|
||||||
const {
|
const {
|
||||||
@@ -25,41 +23,36 @@ const {
|
|||||||
span,
|
span,
|
||||||
} = van.tags;
|
} = 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(() => {
|
van.derive(() => {
|
||||||
if (
|
if (
|
||||||
showImageEditor.val &&
|
showImageEditor.val &&
|
||||||
targetNode.val != undefined &&
|
targetNode.val != undefined &&
|
||||||
targetNode.val.outputs &&
|
targetNode.val.outputs && targetNode.val.type === 'SAM'
|
||||||
targetNode.val.type === "SAM MultiLayer"
|
|
||||||
) {
|
) {
|
||||||
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" ? "IMAGE" : "SAM_PROMPT"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
targetNode.val.graph.change();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,232 +60,185 @@ export function SideBar() {
|
|||||||
const layer_to_delete = van.state("");
|
const layer_to_delete = van.state("");
|
||||||
|
|
||||||
return div(
|
return div(
|
||||||
div(
|
{
|
||||||
{
|
class:
|
||||||
class:
|
"ml-2 z-100 w-fit flex-col flex justify-center absolute top-0 left-0 bottom-0 items-start gap-2",
|
||||||
"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);
|
const layers = Object.entries(imagePromptsMulti.val);
|
||||||
return ul(
|
return ul(
|
||||||
{
|
{
|
||||||
class: "menu bg-base-200 w-56 rounded-box text-base-content ",
|
class: "menu bg-base-200 w-56 rounded-box text-base-content ",
|
||||||
},
|
},
|
||||||
button(
|
layers.length === 0 ? li(a("Empty layer")) : null,
|
||||||
{
|
...layers.map(([key, value]) => {
|
||||||
onclick: () => {
|
return li(
|
||||||
layers.map(([key, value]) => {
|
|
||||||
imagePrompts.val = [];
|
|
||||||
imagePromptsMulti.val[key] = [];
|
|
||||||
});
|
|
||||||
drawSegment([]);
|
|
||||||
updateImagePrompts();
|
|
||||||
},
|
|
||||||
class: "btn btn-ghost normal-case flex",
|
|
||||||
},
|
|
||||||
"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(
|
a(
|
||||||
{
|
{
|
||||||
class: "flex items-center justify-between",
|
class: () =>
|
||||||
|
`normal-case text-start items-start flex items-center justify-between ${
|
||||||
|
selectedLayer.val === key ? "active" : ""
|
||||||
|
}`,
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
my_modal_3.showModal();
|
selectedLayer.val = key;
|
||||||
},
|
imagePrompts.val = imagePromptsMulti.val[key];
|
||||||
},
|
|
||||||
"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(
|
|
||||||
{
|
|
||||||
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());
|
drawSegment(getClicks());
|
||||||
updateImagePrompts();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
button(
|
key,
|
||||||
{
|
div(
|
||||||
type: "button",
|
{},
|
||||||
class:
|
button(
|
||||||
"btn btn-sm btn-circle btn-ghost absolute right-2 top-2",
|
{
|
||||||
onclick: (e) => {
|
class:
|
||||||
e.stopPropagation();
|
"btn btn-circle btn-xs btn-ghost group hover:text-red-500",
|
||||||
my_modal_3.close();
|
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",
|
||||||
h3(
|
"data-inline": "false",
|
||||||
{ class: "font-bold text-lg text-base-content" },
|
})
|
||||||
"Add new layer!"
|
),
|
||||||
),
|
button(
|
||||||
input({
|
{
|
||||||
type: "text",
|
class:
|
||||||
placeholder: "Type here",
|
"btn btn-circle btn-xs btn-ghost group hover:text-red-500",
|
||||||
class: "input input-bordered w-full",
|
onclick: (e) => {
|
||||||
autofocus: true,
|
console.log("delete");
|
||||||
}),
|
e.preventDefault();
|
||||||
button(
|
e.stopPropagation();
|
||||||
{
|
layer_to_delete.val = key;
|
||||||
type: "submit",
|
setTimeout(() => {
|
||||||
class: "btn btn-sm btn-ghost place-self-end",
|
delete_layer_dialog.showModal();
|
||||||
},
|
}, 0);
|
||||||
"Confirm"
|
},
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { ComfyDialog, $el } from '../../scripts/ui.js';
|
|
||||||
|
|
||||||
export class InfoDialog extends ComfyDialog {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.element.classList.add("comfy-normal-modal");
|
|
||||||
}
|
|
||||||
createButtons() {
|
|
||||||
return [
|
|
||||||
$el("button", {
|
|
||||||
type: "button",
|
|
||||||
textContent: "Close",
|
|
||||||
onclick: () => this.close(),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.element.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
show(html) {
|
|
||||||
if (typeof html === "string") {
|
|
||||||
this.textElement.innerHTML = html;
|
|
||||||
} else {
|
|
||||||
this.textElement.replaceChildren(html);
|
|
||||||
}
|
|
||||||
this.element.style.display = "flex";
|
|
||||||
this.element.style.zIndex = 1001;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const infoDialog = new InfoDialog()
|
|
||||||
+89
-376
@@ -5,7 +5,6 @@ import {
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
imagePrompts,
|
imagePrompts,
|
||||||
targetNode,
|
targetNode,
|
||||||
combinePointsNode,
|
|
||||||
fileName,
|
fileName,
|
||||||
embeddings,
|
embeddings,
|
||||||
imagePromptsMulti,
|
imagePromptsMulti,
|
||||||
@@ -13,37 +12,20 @@ import {
|
|||||||
showLoading,
|
showLoading,
|
||||||
loadingCaption,
|
loadingCaption,
|
||||||
alertDialog,
|
alertDialog,
|
||||||
showPreview,
|
|
||||||
shareLoading,
|
|
||||||
previewModelId,
|
|
||||||
embeddingID,
|
|
||||||
enableAutoSegment,
|
|
||||||
samPrompts,
|
|
||||||
} from "./state.js";
|
} from "./state.js";
|
||||||
import { van } from "./van.js";
|
import { van } from "./van.js";
|
||||||
import { app } from "./app.js";
|
import { app } from "./app.js";
|
||||||
import { api } from "./api.js";
|
import { api } from "./api.js";
|
||||||
import { Container } from "./Container.js";
|
import { Container } from "./Container.js";
|
||||||
import { initModel, loadNpyTensor } from "./onnx.js";
|
import { loadNpyTensor } from "./onnx.js";
|
||||||
import "https://code.iconify.design/3/3.1.0/iconify.min.js";
|
import "https://code.iconify.design/3/3.1.0/iconify.min.js";
|
||||||
import {
|
import { drawSegment, getClicks } from "./LayerEditor.js";
|
||||||
autoSegment,
|
|
||||||
drawSegment,
|
|
||||||
getClicks,
|
|
||||||
segmented,
|
|
||||||
} from "./LayerEditor.js";
|
|
||||||
import { infoDialog } from "./dialog.js";
|
|
||||||
import { sharedAvatarLink } from "./AvatarPreview.js";
|
|
||||||
import { updateImagePrompts } from "./LayerEditor.js";
|
|
||||||
import { updateOutputs } from "./SideBar.js";
|
|
||||||
|
|
||||||
export const generatedImages = {};
|
const stylesheet = document.createElement('link')
|
||||||
|
stylesheet.setAttribute('type', "text/css")
|
||||||
const stylesheet = document.createElement("link");
|
stylesheet.setAttribute('rel', "stylesheet")
|
||||||
stylesheet.setAttribute("type", "text/css");
|
stylesheet.setAttribute('href', './avatar-graph-comfyui/tw-styles.css')
|
||||||
stylesheet.setAttribute("rel", "stylesheet");
|
document.head.appendChild(stylesheet)
|
||||||
stylesheet.setAttribute("href", "./avatar-graph-comfyui/tw-styles.css");
|
|
||||||
document.head.appendChild(stylesheet);
|
|
||||||
|
|
||||||
/** @type {import( '../../../web/types/litegraph.js').LGraphGroup} */
|
/** @type {import( '../../../web/types/litegraph.js').LGraphGroup} */
|
||||||
const recomputeInsideNodesOps = LGraphGroup.prototype.recomputeInsideNodes;
|
const recomputeInsideNodesOps = LGraphGroup.prototype.recomputeInsideNodes;
|
||||||
@@ -249,35 +231,15 @@ function getInputWidgetValue(node, inputIndex, widgetName) {
|
|||||||
/** @type {LGraphNode} */
|
/** @type {LGraphNode} */
|
||||||
let nodea = graph._nodes_by_id[targetLink.origin_id];
|
let nodea = graph._nodes_by_id[targetLink.origin_id];
|
||||||
|
|
||||||
while (nodea.type === "Reroute") {
|
while (nodea.type == "Reroute") {
|
||||||
nodea = nodea.getInputNode(0);
|
nodea = nodea.getInputNode(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(targetLink, nodea);
|
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} */
|
/** @type {string} */
|
||||||
const isGeneratedImage = true;
|
return nodea.widgets.find((x) => x.name === widgetName).value;
|
||||||
return [isGeneratedImage, generatedImages[saveImageNode.id]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -285,86 +247,45 @@ function getInputWidgetValue(node, inputIndex, widgetName) {
|
|||||||
* @param {LGraphNode} node
|
* @param {LGraphNode} node
|
||||||
*/
|
*/
|
||||||
function showMyImageEditor(node) {
|
function showMyImageEditor(node) {
|
||||||
let [isGeneratedImage, connectedImageFileName] = getInputWidgetValue(
|
let connectedImageFileName = getInputWidgetValue(node, 0, "image");
|
||||||
node,
|
const split = connectedImageFileName.split("/");
|
||||||
0,
|
if (split.length > 1) connectedImageFileName = split[1];
|
||||||
"image"
|
|
||||||
|
const embeddingFilename = node.widgets.find(
|
||||||
|
(x) => x.name === "embedding_id"
|
||||||
|
).value;
|
||||||
|
|
||||||
|
const v = JSON.parse(
|
||||||
|
node.widgets.find((x) => x.name === "image_prompts_json").value
|
||||||
);
|
);
|
||||||
if (!connectedImageFileName) {
|
|
||||||
alertDialog.val = {
|
if (!Array.isArray(v)) {
|
||||||
text: "Please connect or generate an image first",
|
// this is a multi prompt
|
||||||
time: 3000,
|
imagePromptsMulti.val = v;
|
||||||
};
|
selectedLayer.val = Object.keys(imagePromptsMulti.val)[0];
|
||||||
return;
|
imagePrompts.val = imagePromptsMulti.val[selectedLayer.val];
|
||||||
|
} else {
|
||||||
|
// this is a single prompt
|
||||||
|
selectedLayer.val = "";
|
||||||
|
imagePromptsMulti.val = {};
|
||||||
|
imagePrompts.val = v;
|
||||||
}
|
}
|
||||||
|
showImageEditor.val = true;
|
||||||
loadingCaption.val = "Loading SAM model...";
|
imageUrl.val = api.apiURL(
|
||||||
showLoading.val = true;
|
`/view?filename=${encodeURIComponent(
|
||||||
|
connectedImageFileName
|
||||||
const ckpt = node.widgets.find((x) => x.name === "ckpt").value;
|
)}&type=input&subfolder=${split.length > 1 ? split[0] : ""}`
|
||||||
const modelType = ckpt.match(/vit_[lbh]/)?.[0];
|
);
|
||||||
initModel(modelType).then((res) => {
|
const embeedingUrl = api.apiURL(
|
||||||
loadingCaption.val = "Computing image embedding...";
|
`/view?filename=${encodeURIComponent(
|
||||||
|
`${embeddingFilename}.npy`
|
||||||
const split = connectedImageFileName.split("/");
|
)}&type=output&subfolder=`
|
||||||
let id = connectedImageFileName;
|
);
|
||||||
if (split.length > 1) id = split[1];
|
loadNpyTensor(embeedingUrl).then((tensor) => {
|
||||||
|
embeddings.val = tensor;
|
||||||
node.widgets.find((x) => x.name === "embedding_id").value = id;
|
drawSegment(getClicks());
|
||||||
embeddingID.val = id;
|
|
||||||
|
|
||||||
api
|
|
||||||
.fetchApi("/sam_model", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
image: connectedImageFileName,
|
|
||||||
isGeneratedImage,
|
|
||||||
embedding_id: id,
|
|
||||||
ckpt,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
showLoading.val = false;
|
|
||||||
const v = JSON.parse(
|
|
||||||
node.widgets.find((x) => x.name === "image_prompts_json").value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!Array.isArray(v)) {
|
|
||||||
// this is a multi prompt
|
|
||||||
imagePromptsMulti.val = v;
|
|
||||||
selectedLayer.val = Object.keys(imagePromptsMulti.val)[0];
|
|
||||||
imagePrompts.val = imagePromptsMulti.val[selectedLayer.val];
|
|
||||||
} else {
|
|
||||||
// this is a single prompt
|
|
||||||
selectedLayer.val = "";
|
|
||||||
imagePromptsMulti.val = {};
|
|
||||||
imagePrompts.val = v;
|
|
||||||
}
|
|
||||||
showImageEditor.val = true;
|
|
||||||
const subfolder =
|
|
||||||
isGeneratedImage || split.length === 1 ? "" : split[0];
|
|
||||||
imageUrl.val = api.apiURL(
|
|
||||||
`/view?filename=${encodeURIComponent(connectedImageFileName)}&type=${
|
|
||||||
isGeneratedImage ? "output" : "input"
|
|
||||||
}&subfolder=${subfolder}`
|
|
||||||
);
|
|
||||||
const embeedingUrl = api.apiURL(
|
|
||||||
`/view?filename=${encodeURIComponent(
|
|
||||||
`${id}_${modelType}.npy`
|
|
||||||
)}&type=output&subfolder=`
|
|
||||||
);
|
|
||||||
loadNpyTensor(embeedingUrl).then(async (tensor) => {
|
|
||||||
embeddings.val = tensor;
|
|
||||||
if (enableAutoSegment.val && !segmented.val) await autoSegment();
|
|
||||||
drawSegment(getClicks());
|
|
||||||
updateImagePrompts();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
showLoading.val = false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
targetNode.val = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
|
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
|
||||||
@@ -373,38 +294,47 @@ const ext = {
|
|||||||
getCustomWidgets(app) {
|
getCustomWidgets(app) {
|
||||||
return {
|
return {
|
||||||
SAM_PROMPTS(node, inputName, inputData, app) {
|
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", "", () => {
|
const btn = node.addWidget("button", "Edit prompt", "", () => {
|
||||||
showMyImageEditor(node);
|
let connectedImageFileName = getInputWidgetValue(node, 0, "image");
|
||||||
btn.serialize = false;
|
if (!connectedImageFileName) {
|
||||||
});
|
alertDialog.val = {
|
||||||
|
text: "Please connect an image first",
|
||||||
targetNode.val = node;
|
time: 3000,
|
||||||
node.onConnectInput = (node, slot, targetSlot) => {
|
};
|
||||||
if (targetSlot.name === "SAM_PROMPTS") {
|
return;
|
||||||
imagePromptsMulti.val = samPrompts.val;
|
|
||||||
updateOutputs();
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
loadingCaption.val = "Computing image embedding...";
|
||||||
widget: btn,
|
showLoading.val = true;
|
||||||
};
|
|
||||||
},
|
const split = connectedImageFileName.split("/");
|
||||||
COMBINE_POINTS(node, inputName, inputData, app) {
|
let id = connectedImageFileName;
|
||||||
const btn = node.addWidget("button", "Edit points", "", () => {
|
if (split.length > 1) id = split[1];
|
||||||
console.log("Edit points");
|
|
||||||
combine_points_dialog.showModal();
|
node.widgets.find((x) => x.name === "embedding_id").value = id;
|
||||||
combinePointsNode.val = node;
|
|
||||||
|
const ckpt = node.widgets.find((x) => x.name === "ckpt").value;
|
||||||
|
|
||||||
|
api
|
||||||
|
.fetchApi("/sam_model", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
image: connectedImageFileName,
|
||||||
|
embedding_id: id,
|
||||||
|
ckpt,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
showLoading.val = false;
|
||||||
|
showMyImageEditor(node);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
showLoading.val = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
btn.serialize = false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
widget: btn,
|
widget: btn,
|
||||||
};
|
};
|
||||||
@@ -447,24 +377,6 @@ const ext = {
|
|||||||
widget: btn,
|
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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -481,7 +393,6 @@ const ext = {
|
|||||||
|
|
||||||
node.computeParentGroupResize();
|
node.computeParentGroupResize();
|
||||||
};
|
};
|
||||||
injectUIComponentToComfyuimenu();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async setup() {
|
async setup() {
|
||||||
@@ -494,10 +405,6 @@ const ext = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
api.addEventListener("executed", (evt) => {
|
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) {
|
if (evt.detail?.output.gltfFilename) {
|
||||||
const viewer = document.getElementById(
|
const viewer = document.getElementById(
|
||||||
"avatech-viewer-iframe"
|
"avatech-viewer-iframe"
|
||||||
@@ -535,7 +442,7 @@ const ext = {
|
|||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"keydown",
|
"keydown",
|
||||||
async (event) => {
|
(event) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (my_modal_3.open) {
|
if (my_modal_3.open) {
|
||||||
@@ -543,14 +450,6 @@ const ext = {
|
|||||||
} else {
|
} else {
|
||||||
showImageEditor.val = false;
|
showImageEditor.val = false;
|
||||||
showEditor.val = false;
|
showEditor.val = false;
|
||||||
await uploadSegments();
|
|
||||||
// api.fetchApi("/segments_order", {
|
|
||||||
// method: "POST",
|
|
||||||
// body: JSON.stringify({
|
|
||||||
// name: embeddingID.val,
|
|
||||||
// order: Object.keys(imagePromptsMulti.val),
|
|
||||||
// }),
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -691,7 +590,7 @@ const ext = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "SAM MultiLayer":
|
case "SAM_Prompt_Image":
|
||||||
nodeData.input.required.sam = ["SAM_PROMPTS"];
|
nodeData.input.required.sam = ["SAM_PROMPTS"];
|
||||||
// nodeData.input.required.upload = ['IMAGEUPLOAD'];
|
// nodeData.input.required.upload = ['IMAGEUPLOAD'];
|
||||||
// nodeData.input.required.prompts_points = ["IMAGEUPLOAD"];
|
// nodeData.input.required.prompts_points = ["IMAGEUPLOAD"];
|
||||||
@@ -704,9 +603,10 @@ const ext = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "Combine Points":
|
case "SAM":
|
||||||
nodeData.input.required.sam = ["COMBINE_POINTS"];
|
nodeData.input.required.sam = ["SAM_PROMPTS"];
|
||||||
|
// nodeData.input.required.upload = ['IMAGEUPLOAD'];
|
||||||
|
// nodeData.input.required.prompts_points = ["IMAGEUPLOAD"];
|
||||||
addMenuHandler(nodeType, function (_, options) {
|
addMenuHandler(nodeType, function (_, options) {
|
||||||
options.unshift({
|
options.unshift({
|
||||||
content: "Open In Points Editor (Local)",
|
content: "Open In Points Editor (Local)",
|
||||||
@@ -723,197 +623,10 @@ const ext = {
|
|||||||
nodeData.input.required.obj = ["MESH_GROUP_CONFIG"];
|
nodeData.input.required.obj = ["MESH_GROUP_CONFIG"];
|
||||||
nodeData.input.required.del_obj = ["MESH_GROUP_DELETE"];
|
nodeData.input.required.del_obj = ["MESH_GROUP_DELETE"];
|
||||||
break;
|
break;
|
||||||
case "GroupOps":
|
|
||||||
nodeData.input.required.obj = ["GROUP_OPS"];
|
|
||||||
nodeData.input.required.del_obj = ["GROUP_OPS_DELETE"];
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function uploadPreview() {
|
|
||||||
if (fileName.val == "")
|
|
||||||
app.ui.dialog.show("Please create your avatar first.");
|
|
||||||
else {
|
|
||||||
const file = await fetch(fileName.val)
|
|
||||||
.then((e) => e.arrayBuffer())
|
|
||||||
.then((e) => new Uint8Array(e));
|
|
||||||
const labData = await fetch("https://labs.avatech.ai/api/share", {
|
|
||||||
method: "GET",
|
|
||||||
}).then((e) => e.json());
|
|
||||||
|
|
||||||
await fetch(labData.url, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"x-amz-acl": "public-read",
|
|
||||||
"Content-Type": "model/gltf-binary",
|
|
||||||
"Content-Length": file.length,
|
|
||||||
},
|
|
||||||
body: file,
|
|
||||||
}).catch((error) => console.error(error));
|
|
||||||
|
|
||||||
sharedAvatarLink.val = `https://editor.avatech.ai/viewer?avatarId=${labData?.modelId}`;
|
|
||||||
previewModelId.val = labData.modelId;
|
|
||||||
return labData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectUIComponentToComfyuimenu() {
|
|
||||||
const menu = document.querySelector(".comfy-menu");
|
|
||||||
const avatarPreview = document.createElement("button");
|
|
||||||
avatarPreview.textContent = "Avatar Preview";
|
|
||||||
avatarPreview.onclick = () => {
|
|
||||||
showPreview.val = !showPreview.val;
|
|
||||||
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) => {
|
|
||||||
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";
|
|
||||||
dropdown.onclick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
LiteGraph.closeAllContextMenus();
|
|
||||||
const menu = new LiteGraph.ContextMenu(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
title: "Create new share link",
|
|
||||||
callback: async () => {
|
|
||||||
shareAvatar.textContent = "Loading...";
|
|
||||||
shareAvatar.append(dropdown);
|
|
||||||
|
|
||||||
await uploadPreview();
|
|
||||||
|
|
||||||
shareLoading.val = false;
|
|
||||||
shareAvatar.textContent = "Share Avatar";
|
|
||||||
shareAvatar.append(dropdown);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Update avatar in current share link",
|
|
||||||
callback: async () => {
|
|
||||||
if (!previewModelId.val)
|
|
||||||
app.ui.dialog.show("Please share your avatar first.");
|
|
||||||
else {
|
|
||||||
if (shareLoading.val) return;
|
|
||||||
|
|
||||||
const file = await fetch(fileName.val)
|
|
||||||
.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());
|
|
||||||
|
|
||||||
await fetch(labData.url, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"x-amz-acl": "public-read",
|
|
||||||
"Content-Type": "model/gltf-binary",
|
|
||||||
"Content-Length": file.length,
|
|
||||||
},
|
|
||||||
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));
|
|
||||||
|
|
||||||
infoDialog.show(
|
|
||||||
`Preview updated: <a href='https://editor.avatech.ai/viewer?avatarId=${labData.modelId}' target="_blank">https://editor.avatech.ai/viewer?avatarId=` +
|
|
||||||
labData.modelId +
|
|
||||||
"</a>\n Remember to hard refresh before checking out the new preview!"
|
|
||||||
);
|
|
||||||
|
|
||||||
shareLoading.val = false;
|
|
||||||
shareAvatar.textContent = "Share Avatar";
|
|
||||||
shareAvatar.append(dropdown);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
{
|
|
||||||
event: e,
|
|
||||||
scale: 1.3,
|
|
||||||
},
|
|
||||||
window
|
|
||||||
);
|
|
||||||
menu.root.classList.add("popup");
|
|
||||||
};
|
|
||||||
|
|
||||||
const shareAvatar = document.createElement("button");
|
|
||||||
shareAvatar.textContent = "Share Avatar";
|
|
||||||
shareAvatar.className = "sharebtn";
|
|
||||||
shareAvatar.onclick = async () => {
|
|
||||||
if (shareLoading.val) return;
|
|
||||||
|
|
||||||
if (!previewModelId.val) {
|
|
||||||
shareLoading.val = true;
|
|
||||||
shareAvatar.textContent = "Loading...";
|
|
||||||
shareAvatar.append(dropdown);
|
|
||||||
|
|
||||||
await uploadPreview();
|
|
||||||
|
|
||||||
shareLoading.val = false;
|
|
||||||
shareAvatar.textContent = "Share Avatar";
|
|
||||||
shareAvatar.append(dropdown);
|
|
||||||
} else {
|
|
||||||
infoDialog.show(
|
|
||||||
`Preview avatar url: <a href='https://editor.avatech.ai/viewer?avatarId=${previewModelId.val}' target="_blank">https://editor.avatech.ai/viewer?avatarId=${previewModelId.val}</a>` +
|
|
||||||
`\nChat url: <a href='https://labs.avatech.ai?avatarId=${previewModelId.val}' target="_blank">https://labs.avatech.ai?avatarId=${previewModelId.val}</a>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
menu.append(avatarPreview);
|
|
||||||
menu.append(shareAvatar);
|
|
||||||
menu.append(apiFormat);
|
|
||||||
|
|
||||||
shareAvatar.append(dropdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension(ext);
|
app.registerExtension(ext);
|
||||||
|
|||||||
+14
-12
@@ -1,22 +1,23 @@
|
|||||||
import "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.16.3/dist/ort.min.js";
|
import "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js";
|
||||||
import npyjs from "https://esm.sh/npyjs";
|
import npyjs from "https://esm.sh/npyjs";
|
||||||
import { imageSize } from "./state.js";
|
import { imageSize } from "./state.js";
|
||||||
import { modelData, onnxMaskToImage } from "./onnx_helper.js";
|
import { modelData, onnxMaskToImage } from "./onnx_helper.js";
|
||||||
|
|
||||||
ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.16.3/dist/";
|
ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/";
|
||||||
|
|
||||||
|
// Define image, embedding and model paths
|
||||||
|
const IMAGE_PATH = "/assets/data/dogs.jpg";
|
||||||
|
const IMAGE_EMBEDDING = "/assets/data/dogs_embedding.npy";
|
||||||
|
const MODEL_DIR = "http://127.0.0.1:8188/sam_model";
|
||||||
|
|
||||||
export let model = null;
|
export let model = null;
|
||||||
let modelType = null;
|
|
||||||
|
|
||||||
// Initialize the ONNX model
|
// Initialize the ONNX model
|
||||||
export const initModel = async (type) => {
|
export const initModel = async () => {
|
||||||
try {
|
try {
|
||||||
if (!model || modelType !== type) {
|
if (MODEL_DIR === undefined) return;
|
||||||
modelType = type;
|
const URL = MODEL_DIR;
|
||||||
model = await ort.InferenceSession.create(
|
model = await ort.InferenceSession.create(URL);
|
||||||
`${location.protocol}//${location.host}/sam_model?type=${modelType}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
@@ -24,12 +25,14 @@ export const initModel = async (type) => {
|
|||||||
|
|
||||||
export const loadNpyTensor = async (tensorFile, dType = "float32") => {
|
export const loadNpyTensor = async (tensorFile, dType = "float32") => {
|
||||||
let npLoader = new npyjs();
|
let npLoader = new npyjs();
|
||||||
|
console.log('tensorFile', tensorFile);
|
||||||
const npArray = await npLoader.load(tensorFile);
|
const npArray = await npLoader.load(tensorFile);
|
||||||
|
console.log('np array', npArray);
|
||||||
const tensor = new ort.Tensor(dType, npArray.data, npArray.shape);
|
const tensor = new ort.Tensor(dType, npArray.data, npArray.shape);
|
||||||
return tensor;
|
return tensor;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runONNX = async (clicks, tensor, box) => {
|
export const runONNX = async (clicks, tensor) => {
|
||||||
// console.log('tensor', tensor);
|
// console.log('tensor', tensor);
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
@@ -46,7 +49,6 @@ export const runONNX = async (clicks, tensor, box) => {
|
|||||||
clicks,
|
clicks,
|
||||||
tensor,
|
tensor,
|
||||||
modelScale: imageSize.val,
|
modelScale: imageSize.val,
|
||||||
box,
|
|
||||||
});
|
});
|
||||||
if (feeds === undefined) return;
|
if (feeds === undefined) return;
|
||||||
// Run the SAM ONNX model with the feeds returned from modelData()
|
// Run the SAM ONNX model with the feeds returned from modelData()
|
||||||
|
|||||||
+10
-21
@@ -4,7 +4,7 @@
|
|||||||
// This source code is licensed under the license found in the
|
// This source code is licensed under the license found in the
|
||||||
// LICENSE file in the root directory of this source tree.
|
// LICENSE file in the root directory of this source tree.
|
||||||
|
|
||||||
const modelData = ({ clicks, tensor, modelScale, box }) => {
|
const modelData = ({ clicks, tensor, modelScale }) => {
|
||||||
const imageEmbedding = tensor;
|
const imageEmbedding = tensor;
|
||||||
let pointCoords;
|
let pointCoords;
|
||||||
let pointLabels;
|
let pointLabels;
|
||||||
@@ -18,9 +18,8 @@ const modelData = ({ clicks, tensor, modelScale, box }) => {
|
|||||||
// If there is no box input, a single padding point with
|
// If there is no box input, a single padding point with
|
||||||
// label -1 and coordinates (0.0, 0.0) should be concatenated
|
// label -1 and coordinates (0.0, 0.0) should be concatenated
|
||||||
// so initialize the array to support (n + 1) points.
|
// so initialize the array to support (n + 1) points.
|
||||||
const numPoints = box ? n + 3 : n + 1;
|
pointCoords = new Float32Array(2 * (n + 1));
|
||||||
pointCoords = new Float32Array(2 * numPoints);
|
pointLabels = new Float32Array(n + 1);
|
||||||
pointLabels = new Float32Array(numPoints);
|
|
||||||
|
|
||||||
// Add clicks and scale to what SAM expects
|
// Add clicks and scale to what SAM expects
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
@@ -29,25 +28,15 @@ const modelData = ({ clicks, tensor, modelScale, box }) => {
|
|||||||
pointLabels[i] = clicks[i].clickType;
|
pointLabels[i] = clicks[i].clickType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (box) {
|
// Add in the extra point/label when only clicks and no box
|
||||||
pointCoords[2 * n] = box.x1 * modelScale.samScale;
|
// The extra point is at (0, 0) with label -1
|
||||||
pointCoords[2 * n + 1] = box.y1 * modelScale.samScale;
|
pointCoords[2 * n] = 0.0;
|
||||||
pointLabels[n] = 2;
|
pointCoords[2 * n + 1] = 0.0;
|
||||||
|
pointLabels[n] = -1.0;
|
||||||
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
|
// Create the tensor
|
||||||
pointCoordsTensor = new ort.Tensor("float32", pointCoords, [1, numPoints, 2]);
|
pointCoordsTensor = new ort.Tensor("float32", pointCoords, [1, n + 1, 2]);
|
||||||
pointLabelsTensor = new ort.Tensor("float32", pointLabels, [1, numPoints]);
|
pointLabelsTensor = new ort.Tensor("float32", pointLabels, [1, n + 1]);
|
||||||
}
|
}
|
||||||
const imageSizeTensor = new ort.Tensor("float32", [
|
const imageSizeTensor = new ort.Tensor("float32", [
|
||||||
modelScale.height,
|
modelScale.height,
|
||||||
|
|||||||
+4
-36
@@ -9,42 +9,23 @@
|
|||||||
* @typedef {Object} Point
|
* @typedef {Object} Point
|
||||||
* @property {number} x - The x coordinate
|
* @property {number} x - The x coordinate
|
||||||
* @property {number} y - The y coordinate
|
* @property {number} y - The y coordinate
|
||||||
* @property {>} label - The label
|
* @property {number} label - The label
|
||||||
*
|
|
||||||
* @typedef {Object} Box
|
|
||||||
* @property {number} x1
|
|
||||||
* @property {number} y1
|
|
||||||
* @property {number} x2
|
|
||||||
* @property {number} y2
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { van } from "./van.js";
|
import { van } from "./van.js";
|
||||||
|
|
||||||
export const iframeSrc = van.state("https://editor.avatech.ai?comfyui=true");
|
export const iframeSrc = van.state("https://editor.avatech.ai?comfyui=true");
|
||||||
export const showEditor = van.state(false);
|
export const showEditor = van.state(false);
|
||||||
// localStorage.getItem("showPreview") == 'true'
|
export const previewUrl = van.state("https://editor.avatech.ai/viewer?avatarId=default&hideUI=true&debug=true&width=300&height=300&showAudioControl=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=true&width=350&height=350&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 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 isDirty = van.state(false);
|
||||||
export const fileName = van.state("");
|
export const fileName = van.state('');
|
||||||
export const showImageEditor = van.state(false);
|
export const showImageEditor = van.state(false);
|
||||||
export const showLoading = van.state(false);
|
export const showLoading = van.state(false);
|
||||||
export const alertDialog = van.state({
|
export const alertDialog = van.state({
|
||||||
text: "",
|
text: "",
|
||||||
time: 0,
|
time: 0,
|
||||||
});
|
});
|
||||||
export const shareLoading = van.state(false);
|
|
||||||
export const previewModelId = van.state("");
|
|
||||||
|
|
||||||
export const isGenerateFlow = van.state(false);
|
|
||||||
|
|
||||||
export const loadingCaption = van.state("");
|
export const loadingCaption = van.state("");
|
||||||
export const imageUrl = van.state("");
|
export const imageUrl = van.state("");
|
||||||
@@ -54,17 +35,9 @@ export const imageContainerSize = van.state({
|
|||||||
height: 0,
|
height: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @type {State<Box>} */
|
|
||||||
export const boxes = van.state();
|
|
||||||
|
|
||||||
/** @type {State<Record<string, Box>>} */
|
|
||||||
export const boxesMulti = van.state({});
|
|
||||||
|
|
||||||
/** @type {State<Point[]>} */
|
/** @type {State<Point[]>} */
|
||||||
export const imagePrompts = van.state([]);
|
export const imagePrompts = van.state([]);
|
||||||
|
|
||||||
export const allImagePrompts = van.state([{}]);
|
|
||||||
|
|
||||||
/** @type {State<Record<string, Point[]>>} */
|
/** @type {State<Record<string, Point[]>>} */
|
||||||
export const imagePromptsMulti = van.state({});
|
export const imagePromptsMulti = van.state({});
|
||||||
|
|
||||||
@@ -76,8 +49,3 @@ export const targetNode = van.state();
|
|||||||
|
|
||||||
export const imageSize = van.state({ width: 0, height: 0, samScale: 0 });
|
export const imageSize = van.state({ width: 0, height: 0, samScale: 0 });
|
||||||
export const embeddings = van.state();
|
export const embeddings = van.state();
|
||||||
export const embeddingID = van.state("Test");
|
|
||||||
|
|
||||||
/** @type {State<LGraphNode>} */
|
|
||||||
export const combinePointsNode = van.state();
|
|
||||||
export const samPrompts = van.state({});
|
|
||||||
|
|||||||
+1
-144
@@ -1,146 +1,3 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Gabarito&display=swap');
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
.comfy-normal-modal {
|
|
||||||
display: none; /* Hidden by default */
|
|
||||||
position: fixed; /* Stay in place */
|
|
||||||
z-index: 100; /* Sit on top */
|
|
||||||
padding: 30px 30px 10px 30px;
|
|
||||||
background-color: var(--comfy-menu-bg); /* Modal background */
|
|
||||||
box-shadow: 0 0 20px #888888;
|
|
||||||
border-radius: 10px;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
max-width: 80vw;
|
|
||||||
max-height: 80vh;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
overflow: hidden;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-normal-modal p {
|
|
||||||
overflow: auto;
|
|
||||||
white-space: pre-line; /* This will respect line breaks */
|
|
||||||
margin-bottom: 20px; /* Add some margin between the text and the close button*/
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-normal-modal a {
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-normal-modal button {
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--input-text);
|
|
||||||
background-color: var(--comfy-input-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
border-color: var(--border-color);
|
|
||||||
border-style: solid;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-normal-modal button:hover {
|
|
||||||
filter: brightness(1.2);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sharebtn {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup ~ .litecontextmenu {
|
|
||||||
transform: scale(1.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownbtn {
|
|
||||||
font-size: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 30px;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-top-right-radius: 0.375rem;
|
|
||||||
border-bottom-right-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownbtn:hover {
|
|
||||||
filter: brightness(1.6);
|
|
||||||
background-color: var(--comfy-menu-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-menu > button,
|
|
||||||
.comfy-menu-btns button,
|
|
||||||
.comfy-menu .comfy-list button,
|
|
||||||
.comfy-modal button {
|
|
||||||
color: var(--input-text);
|
|
||||||
background-color: var(--comfy-input-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
border-color: var(--border-color);
|
|
||||||
border-style: solid;
|
|
||||||
margin-top: 2px;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-menu > button:hover,
|
|
||||||
.comfy-menu-btns button:hover,
|
|
||||||
.comfy-menu .comfy-list button:hover,
|
|
||||||
.comfy-modal button:hover,
|
|
||||||
.comfy-settings-btn:hover {
|
|
||||||
filter: brightness(1.2);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-list {
|
|
||||||
color: var(--descrip-text);
|
|
||||||
background-color: var(--comfy-menu-bg);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-color: var(--border-color);
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-list-items {
|
|
||||||
overflow-y: scroll;
|
|
||||||
max-height: 100px;
|
|
||||||
min-height: 25px;
|
|
||||||
background-color: var(--comfy-input-bg);
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-list h4 {
|
|
||||||
min-width: 160px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 3px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-list-items button {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-list-actions {
|
|
||||||
margin: 5px;
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-list-actions button {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
img[src] {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
+460
-1711
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"daisyui": "^4.0.7",
|
"daisyui": "^3.7.5",
|
||||||
"tailwindcss": "^3.3.3"
|
"tailwindcss": "^3.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+207
-12
@@ -8,9 +8,12 @@ devDependencies:
|
|||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^3.5.3
|
specifier: ^3.5.3
|
||||||
version: 3.5.3
|
version: 3.5.3
|
||||||
|
concurrently:
|
||||||
|
specifier: ^8.2.1
|
||||||
|
version: 8.2.1
|
||||||
daisyui:
|
daisyui:
|
||||||
specifier: ^4.0.7
|
specifier: ^3.7.5
|
||||||
version: 4.0.7(postcss@8.4.29)
|
version: 3.7.5
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.3.3
|
specifier: ^3.3.3
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
@@ -22,6 +25,13 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@babel/runtime@7.22.11:
|
||||||
|
resolution: {integrity: sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime: 0.14.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@jridgewell/gen-mapping@0.3.3:
|
/@jridgewell/gen-mapping@0.3.3:
|
||||||
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -73,6 +83,18 @@ packages:
|
|||||||
fastq: 1.15.0
|
fastq: 1.15.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/any-promise@1.3.0:
|
/any-promise@1.3.0:
|
||||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -117,6 +139,14 @@ packages:
|
|||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/chalk@4.1.2:
|
||||||
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/chokidar@3.5.3:
|
/chokidar@3.5.3:
|
||||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
@@ -132,6 +162,30 @@ packages:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cliui@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/colord@2.9.3:
|
||||||
|
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/commander@4.1.1:
|
/commander@4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -141,6 +195,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/concurrently@8.2.1:
|
||||||
|
resolution: {integrity: sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==}
|
||||||
|
engines: {node: ^14.13.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
date-fns: 2.30.0
|
||||||
|
lodash: 4.17.21
|
||||||
|
rxjs: 7.8.1
|
||||||
|
shell-quote: 1.8.1
|
||||||
|
spawn-command: 0.0.2
|
||||||
|
supports-color: 8.1.1
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
yargs: 17.7.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/css-selector-tokenizer@0.8.0:
|
/css-selector-tokenizer@0.8.0:
|
||||||
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
|
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -154,21 +224,24 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/culori@3.2.0:
|
/daisyui@3.7.5:
|
||||||
resolution: {integrity: sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==}
|
resolution: {integrity: sha512-udhiBJYVvcPGXa+mL5IElke6EdddKecjbbz6m43+9IVqzK8GBwetg092Edclo42TkNboLD9nzodeesJygqZt2A==}
|
||||||
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'}
|
engines: {node: '>=16.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
colord: 2.9.3
|
||||||
css-selector-tokenizer: 0.8.0
|
css-selector-tokenizer: 0.8.0
|
||||||
culori: 3.2.0
|
postcss: 8.4.29
|
||||||
picocolors: 1.0.0
|
|
||||||
postcss-js: 4.0.1(postcss@8.4.29)
|
postcss-js: 4.0.1(postcss@8.4.29)
|
||||||
|
tailwindcss: 3.3.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- postcss
|
- ts-node
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/date-fns@2.30.0:
|
||||||
|
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||||
|
engines: {node: '>=0.11'}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.11
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/didyoumean@1.2.2:
|
/didyoumean@1.2.2:
|
||||||
@@ -179,6 +252,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/escalade@3.1.1:
|
||||||
|
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fast-glob@3.3.1:
|
/fast-glob@3.3.1:
|
||||||
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
|
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
|
||||||
engines: {node: '>=8.6.0'}
|
engines: {node: '>=8.6.0'}
|
||||||
@@ -223,6 +305,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/get-caller-file@2.0.5:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/glob-parent@5.1.2:
|
/glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -248,6 +335,11 @@ packages:
|
|||||||
path-is-absolute: 1.0.1
|
path-is-absolute: 1.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/has-flag@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/has@1.0.3:
|
/has@1.0.3:
|
||||||
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
@@ -284,6 +376,11 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-glob@4.0.3:
|
/is-glob@4.0.3:
|
||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -310,6 +407,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash@4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/merge2@1.4.1:
|
/merge2@1.4.1:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -479,6 +580,15 @@ packages:
|
|||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/regenerator-runtime@0.14.0:
|
||||||
|
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/require-directory@2.1.1:
|
||||||
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/resolve@1.22.4:
|
/resolve@1.22.4:
|
||||||
resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==}
|
resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -499,11 +609,41 @@ packages:
|
|||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/rxjs@7.8.1:
|
||||||
|
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/shell-quote@1.8.1:
|
||||||
|
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/source-map-js@1.0.2:
|
/source-map-js@1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/spawn-command@0.0.2:
|
||||||
|
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sucrase@3.34.0:
|
/sucrase@3.34.0:
|
||||||
resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==}
|
resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -518,6 +658,20 @@ packages:
|
|||||||
ts-interface-checker: 0.1.13
|
ts-interface-checker: 0.1.13
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/supports-color@8.1.1:
|
||||||
|
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/supports-preserve-symlinks-flag@1.0.0:
|
/supports-preserve-symlinks-flag@1.0.0:
|
||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -574,19 +728,60 @@ packages:
|
|||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tree-kill@1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ts-interface-checker@0.1.13:
|
/ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tslib@2.6.2:
|
||||||
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/wrappy@1.0.2:
|
/wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/y18n@5.0.8:
|
||||||
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/yaml@2.3.2:
|
/yaml@2.3.2:
|
||||||
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/yargs-parser@21.1.1:
|
||||||
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/yargs@17.7.2:
|
||||||
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
cliui: 8.0.1
|
||||||
|
escalade: 3.1.1
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
string-width: 4.2.3
|
||||||
|
y18n: 5.0.8
|
||||||
|
yargs-parser: 21.1.1
|
||||||
|
dev: true
|
||||||
|
|||||||
+1
-3
@@ -3,9 +3,7 @@ numpy
|
|||||||
opencv-python
|
opencv-python
|
||||||
opencv-contrib-python
|
opencv-contrib-python
|
||||||
einops
|
einops
|
||||||
bpy==3.6.0
|
bpy
|
||||||
segment-anything
|
segment-anything
|
||||||
tqdm
|
tqdm
|
||||||
python-dotenv
|
|
||||||
mediapipe
|
|
||||||
# -e git+https://github.com/facebookresearch/segment-anything.git#egg=segment_anything
|
# -e git+https://github.com/facebookresearch/segment-anything.git#egg=segment_anything
|
||||||
@@ -1,25 +1,13 @@
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from dotenv import load_dotenv
|
from segment_anything import sam_model_registry, SamPredictor
|
||||||
from blender.mesh_utils import upload_avatar_file
|
from PIL import Image, ImageOps
|
||||||
from sam_utils import (
|
|
||||||
sam_ckpt_to_type,
|
|
||||||
compute_image_embedding,
|
|
||||||
check_embedding_exists,
|
|
||||||
save_embedding,
|
|
||||||
load_image,
|
|
||||||
)
|
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import folder_paths
|
import folder_paths
|
||||||
import json
|
import json
|
||||||
|
import numpy as np
|
||||||
import server
|
import server
|
||||||
import base64
|
import re
|
||||||
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
|
# For speeding up ONNX model, see https://github.com/facebookresearch/segment-anything/tree/main/demo#onnx-multithreading-with-sharedarraybuffer
|
||||||
def inject_headers(original_handler):
|
def inject_headers(original_handler):
|
||||||
@@ -44,328 +32,59 @@ for item in server.PromptServer.instance.routes._items:
|
|||||||
routes.append(item)
|
routes.append(item)
|
||||||
server.PromptServer.instance.routes._items = routes
|
server.PromptServer.instance.routes._items = routes
|
||||||
|
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get("/avatar-graph-comfyui/tw-styles.css")
|
@server.PromptServer.instance.routes.get("/avatar-graph-comfyui/tw-styles.css")
|
||||||
async def get_web_styles(request):
|
async def get_web_styles(request):
|
||||||
filename = os.path.join(os.path.dirname(__file__), "js/tw-styles.css")
|
filename = os.path.join(os.path.dirname(__file__), "js/tw-styles.css")
|
||||||
return web.FileResponse(filename)
|
return web.FileResponse(filename)
|
||||||
|
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get("/sam_model")
|
@server.PromptServer.instance.routes.get("/sam_model")
|
||||||
async def get_sam_model(request):
|
async def get_sam_model(request):
|
||||||
model_type = request.rel_url.query.get("type", "vit_h")
|
filename = os.path.join(folder_paths.base_path, "web/models/sam.onnx")
|
||||||
filename = os.path.join(folder_paths.base_path, f"web/models/sam_{model_type}.onnx")
|
# print(filename)
|
||||||
if not os.path.isfile(filename):
|
if not os.path.isfile(filename):
|
||||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
print(f"Downloading ONNX model to {filename}")
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/models/sam_{model_type}.onnx"
|
"https://avatech-avatar-dev1.nyc3.cdn.digitaloceanspaces.com/models/sam.onnx"
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
with open(filename, "wb") as f:
|
with open(filename, "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
print(f"ONNX model downloaded")
|
print(f"ONNX model downloaded: {filename}")
|
||||||
return web.FileResponse(filename)
|
return web.FileResponse(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def load_image(image):
|
||||||
|
image_path = folder_paths.get_annotated_filepath(image)
|
||||||
|
i = Image.open(image_path)
|
||||||
|
i = ImageOps.exif_transpose(i)
|
||||||
|
image = i.convert("RGB")
|
||||||
|
image = np.array(image).astype(np.float32) / 255.0
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.post("/sam_model")
|
@server.PromptServer.instance.routes.post("/sam_model")
|
||||||
async def post_sam_model(request):
|
async def post_sam_model(request):
|
||||||
post = await request.json()
|
post = await request.json()
|
||||||
is_generated_image = post.get("isGeneratedImage")
|
|
||||||
emb_id = post.get("embedding_id")
|
emb_id = post.get("embedding_id")
|
||||||
ckpt = post.get("ckpt")
|
emb_filename = f"{folder_paths.get_output_directory()}/{emb_id}.npy"
|
||||||
model_type = sam_ckpt_to_type[ckpt]
|
if not os.path.exists(emb_filename):
|
||||||
if not check_embedding_exists(emb_id, model_type):
|
image = load_image(post.get("image"))
|
||||||
image = load_image(post.get("image"), is_generated_image)
|
ckpt = post.get("ckpt")
|
||||||
emb, img_model_input_size, img_original_size = compute_image_embedding(
|
model_type = re.findall(r'vit_[lbh]', ckpt)[0]
|
||||||
image, model_type
|
ckpt = folder_paths.get_full_path("sams", ckpt)
|
||||||
)
|
sam = sam_model_registry[model_type](checkpoint=ckpt)
|
||||||
save_embedding(emb_id, model_type, emb, img_model_input_size, img_original_size)
|
predictor = SamPredictor(sam)
|
||||||
print("Finished embedding")
|
|
||||||
return web.json_response({})
|
|
||||||
|
|
||||||
|
image_np = (image * 255).astype(np.uint8)
|
||||||
def save_image(image, save_name=None):
|
predictor.set_image(image_np)
|
||||||
input_folder = folder_paths.get_input_directory()
|
emb = predictor.get_image_embedding().cpu().numpy()
|
||||||
name, extension = os.path.splitext(image.filename)
|
np.save(emb_filename, emb)
|
||||||
|
with open(f"{folder_paths.get_output_directory()}/{emb_id}.json", "w") as f:
|
||||||
if save_name == None:
|
json.dump(
|
||||||
save_name = f"{name}{extension}"
|
{
|
||||||
i = 1
|
"input_size": predictor.input_size,
|
||||||
while os.path.exists(f"{input_folder}/{save_name}"):
|
"original_size": predictor.original_size,
|
||||||
save_name = f"{name}_{i}{extension}"
|
},
|
||||||
i += 1
|
f,
|
||||||
|
|
||||||
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 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())
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
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(f'"{key}"', f'"{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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
# @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")
|
|
||||||
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(output_dir, f"{key}.png")
|
|
||||||
with open(filename, "wb") as f:
|
|
||||||
f.write(base64.b64decode(value.split(",")[1]))
|
|
||||||
|
|
||||||
order = list(segments.keys())
|
|
||||||
with open(os.path.join(output_dir, "order.json"), "w") as f:
|
|
||||||
json.dump(order, f)
|
|
||||||
return web.json_response({})
|
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})
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
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"}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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"}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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"}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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"}
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
# For auto-segmentation
|
|
||||||
import mediapipe as mp
|
|
||||||
import numpy as np
|
|
||||||
import os
|
|
||||||
from math import sqrt
|
|
||||||
|
|
||||||
face_landmarker = None
|
|
||||||
pose_landmarker = None
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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]],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load_mediapipe_models():
|
|
||||||
global face_landmarker, pose_landmarker
|
|
||||||
if face_landmarker is None and pose_landmarker is None:
|
|
||||||
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 face_landmarker, pose_landmarker
|
|
||||||
|
|
||||||
|
|
||||||
def auto_segment_face(image, face_landmarks):
|
|
||||||
H, W, C = image.shape
|
|
||||||
layer_points = {}
|
|
||||||
layer_bboxes = {}
|
|
||||||
|
|
||||||
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"]:
|
|
||||||
layer_points[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,
|
|
||||||
}
|
|
||||||
|
|
||||||
layer_points[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),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
layer_bboxes[key] = box
|
|
||||||
|
|
||||||
return layer_points, layer_bboxes
|
|
||||||
|
|
||||||
|
|
||||||
def auto_segment_pose(image, pose_landmarks):
|
|
||||||
H, W, C = image.shape
|
|
||||||
layer_points = {}
|
|
||||||
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
|
|
||||||
layer_points["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 layer_points
|
|
||||||
|
|
||||||
|
|
||||||
def detect_face(np_image):
|
|
||||||
face_landmarker, pose_landmarker = load_mediapipe_models()
|
|
||||||
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
|
|
||||||
if len(face_landmarks) > 0:
|
|
||||||
face_points, face_bboxes = auto_segment_face(np_image, face_landmarks[0])
|
|
||||||
else:
|
|
||||||
face_points, face_bboxes = {}, {}
|
|
||||||
print("Warning: no face detected")
|
|
||||||
|
|
||||||
pose_landmarks = pose_landmarker.detect(mp_image).pose_landmarks
|
|
||||||
if len(pose_landmarks) > 0:
|
|
||||||
pose_points = auto_segment_pose(np_image, pose_landmarks[0])
|
|
||||||
else:
|
|
||||||
pose_points = {}
|
|
||||||
print("Warning: no pose detected")
|
|
||||||
|
|
||||||
layer_points = {**face_points, **pose_points}
|
|
||||||
layer_bboxes = {**face_bboxes}
|
|
||||||
return layer_points, layer_bboxes
|
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
import folder_paths
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import re
|
||||||
|
from segment_anything import sam_model_registry, SamPredictor
|
||||||
|
from einops import rearrange, repeat
|
||||||
|
|
||||||
|
|
||||||
|
global_predictor = None
|
||||||
|
|
||||||
|
class SAM:
|
||||||
|
def __init__(self):
|
||||||
|
self.predictor = None
|
||||||
|
self.output_dir = folder_paths.get_output_directory()
|
||||||
|
|
||||||
|
@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",),
|
||||||
|
"ckpt": (folder_paths.get_filename_list("sams"),),
|
||||||
|
"embedding_id": (
|
||||||
|
"STRING",
|
||||||
|
{"multiline": False, "default": "embedding"},
|
||||||
|
),
|
||||||
|
# "image": (sorted(files), ),
|
||||||
|
"image_prompts_json": ("STRING", {"multiline": False, "default": "[]"}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORY = "image"
|
||||||
|
|
||||||
|
RETURN_TYPES = ("SAM_PROMPT",)
|
||||||
|
FUNCTION = "load_image"
|
||||||
|
|
||||||
|
def load_image(self, image, ckpt, embedding_id, image_prompts_json):
|
||||||
|
import json
|
||||||
|
|
||||||
|
global global_predictor
|
||||||
|
|
||||||
|
if global_predictor is None:
|
||||||
|
ckpt = folder_paths.get_full_path("sams", ckpt)
|
||||||
|
model_type = re.findall(r'vit_[lbh]', ckpt)[0]
|
||||||
|
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}.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)
|
||||||
|
|
||||||
|
with open(f"{self.output_dir}/{embedding_id}.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)
|
||||||
|
|
||||||
|
with open(f"{self.output_dir}/{embedding_id}.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"]
|
||||||
|
|
||||||
|
image_prompts = json.loads(image_prompts_json)
|
||||||
|
|
||||||
|
result = [image_prompts]
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
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)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {"SAM": SAM}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {"SAM": "Segmentation (SAM)"}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import folder_paths
|
|
||||||
import os
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
from sam_utils import (
|
|
||||||
load_model,
|
|
||||||
check_embedding_exists,
|
|
||||||
compute_image_embedding,
|
|
||||||
save_embedding,
|
|
||||||
load_embdding,
|
|
||||||
load_image,
|
|
||||||
)
|
|
||||||
from mediapipe_utils import detect_face
|
|
||||||
from einops import rearrange, repeat
|
|
||||||
|
|
||||||
|
|
||||||
class SAMMultiLayer:
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(s):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"image": ("IMAGE",),
|
|
||||||
"ckpt": (folder_paths.get_filename_list("sams"),),
|
|
||||||
"embedding_id": (
|
|
||||||
"STRING",
|
|
||||||
{"multiline": False, "default": "embedding"},
|
|
||||||
),
|
|
||||||
"image_prompts_json": ("STRING", {"multiline": False, "default": "[]"}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
CATEGORY = "image"
|
|
||||||
|
|
||||||
RETURN_TYPES = ["SAM_PROMPT"] # + ["IMAGE"] * 100
|
|
||||||
FUNCTION = "run"
|
|
||||||
|
|
||||||
def run(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())
|
|
||||||
layer_points = json.loads(image_prompts_json.replace("'", '"'))
|
|
||||||
|
|
||||||
order_file = (
|
|
||||||
f"{folder_paths.get_output_directory()}/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)
|
|
||||||
|
|
||||||
result = [layer_points]
|
|
||||||
for segment in order:
|
|
||||||
image = load_image(
|
|
||||||
f"{folder_paths.get_output_directory()}/segments_{embedding_id}/{segment}.png",
|
|
||||||
comfyui_format=True,
|
|
||||||
)
|
|
||||||
result.append(image)
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
# get first image from batch
|
|
||||||
image = image[0]
|
|
||||||
if image.shape[2] == 4:
|
|
||||||
# to RGB
|
|
||||||
image = image[:, :, :3]
|
|
||||||
|
|
||||||
if not check_embedding_exists(embedding_id, model_type):
|
|
||||||
emb, img_model_input_size, img_original_size = compute_image_embedding(
|
|
||||||
image, model_type
|
|
||||||
)
|
|
||||||
save_embedding(
|
|
||||||
embedding_id,
|
|
||||||
model_type,
|
|
||||||
emb,
|
|
||||||
img_model_input_size,
|
|
||||||
img_original_size,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
load_embdding(embedding_id, model_type)
|
|
||||||
|
|
||||||
detected_points, detected_bboxes = detect_face(image.numpy())
|
|
||||||
result = [layer_points]
|
|
||||||
for layer, points in layer_points.items():
|
|
||||||
if detected_points is not None and layer in detected_points:
|
|
||||||
# use detected points by mediapipe
|
|
||||||
points = detected_points[layer]
|
|
||||||
|
|
||||||
if len(points) == 0:
|
|
||||||
# no points, append a black image
|
|
||||||
h, w, c = image.shape
|
|
||||||
result.append(torch.zeros(1, h, w, c))
|
|
||||||
print("No points for layer", layer)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# prepare for SAM inferencing
|
|
||||||
point_coords = np.array([[p["x"], p["y"]] for p in points])
|
|
||||||
point_labels = np.array([p["label"] for p in points])
|
|
||||||
bbox = (
|
|
||||||
detected_bboxes[layer]
|
|
||||||
if detected_bboxes is not None and layer in detected_bboxes
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
sam_predictor = load_model(model_type)["predictor"]
|
|
||||||
masks, _, _ = sam_predictor.predict(
|
|
||||||
point_coords=point_coords,
|
|
||||||
point_labels=point_labels,
|
|
||||||
box=bbox,
|
|
||||||
)
|
|
||||||
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.unsqueeze(0)
|
|
||||||
result.append(out_image)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {"SAM MultiLayer": SAMMultiLayer}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {"SAM MultiLayer": "SAM MultiLayer"}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
from segment_anything import sam_model_registry, SamPredictor
|
|
||||||
from PIL import Image, ImageOps
|
|
||||||
import folder_paths
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
sam_type_to_ckpt = {
|
|
||||||
"vit_h": "sam_vit_h_4b8939.pth",
|
|
||||||
"vit_l": "sam_vit_l_0b3195.pth",
|
|
||||||
"vit_b": "sam_vit_b_01ec64.pth",
|
|
||||||
}
|
|
||||||
sam_ckpt_to_type = {v: k for k, v in sam_type_to_ckpt.items()}
|
|
||||||
|
|
||||||
sam_instance = {"model_type": None, "model": None, "predictor": None}
|
|
||||||
|
|
||||||
|
|
||||||
def load_model(model_type):
|
|
||||||
global sam_instance
|
|
||||||
if sam_instance["model"] is None or sam_instance["model_type"] != model_type:
|
|
||||||
ckpt = sam_type_to_ckpt[model_type]
|
|
||||||
ckpt = folder_paths.get_full_path("sams", ckpt)
|
|
||||||
sam_instance["model_type"] = model_type
|
|
||||||
sam_instance["model"] = sam_model_registry[model_type](checkpoint=ckpt)
|
|
||||||
if torch.cuda.is_available():
|
|
||||||
sam_instance["model"].cuda()
|
|
||||||
sam_instance["predictor"] = SamPredictor(sam_instance["model"])
|
|
||||||
return sam_instance
|
|
||||||
|
|
||||||
|
|
||||||
def check_embedding_exists(emb_id, model_type):
|
|
||||||
emb_filename = f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.npy"
|
|
||||||
return os.path.exists(emb_filename)
|
|
||||||
|
|
||||||
|
|
||||||
def save_embedding(emb_id, model_type, emb, img_input_size, img_original_size):
|
|
||||||
emb_filename = f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.npy"
|
|
||||||
np.save(emb_filename, emb)
|
|
||||||
|
|
||||||
json_filename = f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.json"
|
|
||||||
with open(json_filename, "w") as f:
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
"input_size": img_input_size,
|
|
||||||
"original_size": img_original_size,
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_embdding(emb_id, model_type):
|
|
||||||
emb_filename = f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.npy"
|
|
||||||
emb = np.load(emb_filename)
|
|
||||||
|
|
||||||
json_filename = f"{folder_paths.get_output_directory()}/{emb_id}_{model_type}.json"
|
|
||||||
with open(json_filename, "r") as f:
|
|
||||||
sizes = json.load(f)
|
|
||||||
|
|
||||||
predictor = load_model(model_type)["predictor"]
|
|
||||||
predictor.input_size = sizes["input_size"]
|
|
||||||
predictor.features = torch.from_numpy(emb)
|
|
||||||
predictor.is_image_set = True
|
|
||||||
predictor.original_size = sizes["original_size"]
|
|
||||||
|
|
||||||
|
|
||||||
@torch.no_grad()
|
|
||||||
def compute_image_embedding(image, model_type="vit_h"):
|
|
||||||
sam = load_model(model_type)
|
|
||||||
predictor = sam["predictor"]
|
|
||||||
|
|
||||||
# if image.shape[3] == 4:
|
|
||||||
# image = image[:, :, :, :3]
|
|
||||||
|
|
||||||
if torch.is_tensor(image):
|
|
||||||
image = image.numpy()
|
|
||||||
|
|
||||||
image_np = (image * 255).astype(np.uint8)
|
|
||||||
predictor.set_image(image_np)
|
|
||||||
emb = predictor.get_image_embedding().cpu().numpy()
|
|
||||||
|
|
||||||
return emb, predictor.input_size, predictor.original_size
|
|
||||||
|
|
||||||
|
|
||||||
def load_image(image, is_generated_image=False, comfyui_format=False):
|
|
||||||
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")
|
|
||||||
image = np.array(image).astype(np.float32) / 255.0
|
|
||||||
if comfyui_format:
|
|
||||||
# to torch and create batch dimension
|
|
||||||
image = torch.from_numpy(image)[None,]
|
|
||||||
return image
|
|
||||||
@@ -3,9 +3,6 @@ module.exports = {
|
|||||||
// content: ['./js/**/*.{html,js}'],
|
// content: ['./js/**/*.{html,js}'],
|
||||||
content: ['./js/**/*.{html,js}'],
|
content: ['./js/**/*.{html,js}'],
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
|
||||||
'gabarito': ['Gabarito'],
|
|
||||||
},
|
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
daisyui: {
|
daisyui: {
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 316,
|
||||||
|
"last_link_id": 561,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 181,
|
||||||
|
"type": "CheckpointLoaderSimple",
|
||||||
|
"pos": [
|
||||||
|
40,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 340.6639709472656,
|
||||||
|
"1": 98
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "MODEL",
|
||||||
|
"type": "MODEL",
|
||||||
|
"links": [
|
||||||
|
552
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CLIP",
|
||||||
|
"type": "CLIP",
|
||||||
|
"links": [
|
||||||
|
527,
|
||||||
|
558
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VAE",
|
||||||
|
"type": "VAE",
|
||||||
|
"links": [
|
||||||
|
249,
|
||||||
|
547
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CheckpointLoaderSimple"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"meinamix_meinaV10.safetensors"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 176,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
900,
|
||||||
|
230
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 326.9862060546875,
|
||||||
|
"1": 262.3746643066406
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": 552
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 557,
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 554
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 540,
|
||||||
|
"slot_index": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
248
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
509878269555324,
|
||||||
|
"randomize",
|
||||||
|
25,
|
||||||
|
7,
|
||||||
|
"dpmpp_2m",
|
||||||
|
"karras",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 175,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
540,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 304.70074462890625,
|
||||||
|
"1": 109.89002990722656
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 527
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
554
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"worst quality,low quality, normal quality, monochrome, grayscale, closed mouth, extended tongue,sad"
|
||||||
|
],
|
||||||
|
"color": "#322",
|
||||||
|
"bgcolor": "#533"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 174,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
540,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 299.78692626953125,
|
||||||
|
"1": 101.67720031738281
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 558,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
557
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"open mouth,detailed mouth,lips,smile"
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 301,
|
||||||
|
"type": "VAEEncodeForInpaint",
|
||||||
|
"pos": [
|
||||||
|
538,
|
||||||
|
470
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 315,
|
||||||
|
"1": 98
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 4,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "pixels",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 559,
|
||||||
|
"slot_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vae",
|
||||||
|
"type": "VAE",
|
||||||
|
"link": 547,
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mask",
|
||||||
|
"type": "MASK",
|
||||||
|
"link": 560,
|
||||||
|
"slot_index": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
540
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "VAEEncodeForInpaint"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
6
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 315,
|
||||||
|
"type": "LoadImage",
|
||||||
|
"pos": [
|
||||||
|
61,
|
||||||
|
455
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 315,
|
||||||
|
"1": 314.0001525878906
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
559
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MASK",
|
||||||
|
"type": "MASK",
|
||||||
|
"links": [
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "LoadImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"clipspace/clipspace-mask-186477.83499991894.png [input]",
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 184,
|
||||||
|
"type": "VAEDecode",
|
||||||
|
"pos": [
|
||||||
|
1300,
|
||||||
|
230
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 210,
|
||||||
|
"1": 46
|
||||||
|
},
|
||||||
|
"flags": {
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
"order": 6,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "samples",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 248
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vae",
|
||||||
|
"type": "VAE",
|
||||||
|
"link": 249,
|
||||||
|
"slot_index": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
561
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "VAEDecode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 316,
|
||||||
|
"type": "PreviewImage",
|
||||||
|
"pos": [
|
||||||
|
1571.9339136962892,
|
||||||
|
243.9844624023438
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 210,
|
||||||
|
"1": 26
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 7,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 561
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "PreviewImage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
248,
|
||||||
|
176,
|
||||||
|
0,
|
||||||
|
184,
|
||||||
|
0,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
249,
|
||||||
|
181,
|
||||||
|
2,
|
||||||
|
184,
|
||||||
|
1,
|
||||||
|
"VAE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
527,
|
||||||
|
181,
|
||||||
|
1,
|
||||||
|
175,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
540,
|
||||||
|
301,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
3,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
547,
|
||||||
|
181,
|
||||||
|
2,
|
||||||
|
301,
|
||||||
|
1,
|
||||||
|
"VAE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
552,
|
||||||
|
181,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
0,
|
||||||
|
"MODEL"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
554,
|
||||||
|
175,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
2,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
557,
|
||||||
|
174,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
1,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
558,
|
||||||
|
181,
|
||||||
|
1,
|
||||||
|
174,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
559,
|
||||||
|
315,
|
||||||
|
0,
|
||||||
|
301,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
560,
|
||||||
|
315,
|
||||||
|
1,
|
||||||
|
301,
|
||||||
|
2,
|
||||||
|
"MASK"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
561,
|
||||||
|
184,
|
||||||
|
0,
|
||||||
|
316,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 318,
|
||||||
|
"last_link_id": 563,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 184,
|
||||||
|
"type": "VAEDecode",
|
||||||
|
"pos": [
|
||||||
|
1290,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 210,
|
||||||
|
"1": 46
|
||||||
|
},
|
||||||
|
"flags": {
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
"order": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "samples",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 248
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vae",
|
||||||
|
"type": "VAE",
|
||||||
|
"link": 249,
|
||||||
|
"slot_index": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
562
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "VAEDecode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 317,
|
||||||
|
"type": "SaveImage",
|
||||||
|
"pos": [
|
||||||
|
1530,
|
||||||
|
420
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
270
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 6,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 562
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {},
|
||||||
|
"widgets_values": [
|
||||||
|
"ComfyUI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 175,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
530,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
301.38145862926115,
|
||||||
|
151.58555811101718
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 527
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
554
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"worst quality, low quality, normal quality, monochrome, grayscale, closed mouth, extended tongue, sad, text, signature, username, artist name, animal, big mouth,leg,hand"
|
||||||
|
],
|
||||||
|
"color": "#322",
|
||||||
|
"bgcolor": "#533"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 181,
|
||||||
|
"type": "CheckpointLoaderSimple",
|
||||||
|
"pos": [
|
||||||
|
100,
|
||||||
|
390
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 340.6639709472656,
|
||||||
|
"1": 98
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "MODEL",
|
||||||
|
"type": "MODEL",
|
||||||
|
"links": [
|
||||||
|
552
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CLIP",
|
||||||
|
"type": "CLIP",
|
||||||
|
"links": [
|
||||||
|
527,
|
||||||
|
558
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VAE",
|
||||||
|
"type": "VAE",
|
||||||
|
"links": [
|
||||||
|
249
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CheckpointLoaderSimple"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"meinamix_meinaV11.safetensors"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 174,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
530,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
294.7698808149855,
|
||||||
|
129.02360742742354
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 558,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
557
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"looking at viewer, detailed face, open mouth, smile, solo,eye-level angle,\nsimple background, 1 girl, half body"
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 318,
|
||||||
|
"type": "EmptyLatentImage",
|
||||||
|
"pos": [
|
||||||
|
530,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
299.24921209161903,
|
||||||
|
106
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
563
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "EmptyLatentImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
512,
|
||||||
|
768,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 176,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
890,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 326.9862060546875,
|
||||||
|
"1": 262.3746643066406
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 4,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": 552
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 557,
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 554
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 563,
|
||||||
|
"slot_index": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
248
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
24285104147823,
|
||||||
|
"randomize",
|
||||||
|
25,
|
||||||
|
5,
|
||||||
|
"dpmpp_2m",
|
||||||
|
"karras",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
248,
|
||||||
|
176,
|
||||||
|
0,
|
||||||
|
184,
|
||||||
|
0,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
249,
|
||||||
|
181,
|
||||||
|
2,
|
||||||
|
184,
|
||||||
|
1,
|
||||||
|
"VAE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
527,
|
||||||
|
181,
|
||||||
|
1,
|
||||||
|
175,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
552,
|
||||||
|
181,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
0,
|
||||||
|
"MODEL"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
554,
|
||||||
|
175,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
2,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
557,
|
||||||
|
174,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
1,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
558,
|
||||||
|
181,
|
||||||
|
1,
|
||||||
|
174,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
562,
|
||||||
|
184,
|
||||||
|
0,
|
||||||
|
317,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
563,
|
||||||
|
318,
|
||||||
|
0,
|
||||||
|
176,
|
||||||
|
3,
|
||||||
|
"LATENT"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,669 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 23,
|
||||||
|
"last_link_id": 27,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"type": "ControlNetApply",
|
||||||
|
"pos": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 317.4000244140625,
|
||||||
|
"1": 98
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 8,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "conditioning",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "control_net",
|
||||||
|
"type": "CONTROL_NET",
|
||||||
|
"link": 22,
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 26,
|
||||||
|
"slot_index": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
24
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "ControlNetApply"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
0.75
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "ControlNetApply",
|
||||||
|
"pos": [
|
||||||
|
1020,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 317.4000244140625,
|
||||||
|
"1": 98
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 9,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "conditioning",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 24,
|
||||||
|
"slot_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "control_net",
|
||||||
|
"type": "CONTROL_NET",
|
||||||
|
"link": 8,
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "ControlNetApply"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
0.75
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"type": "VAEDecode",
|
||||||
|
"pos": [
|
||||||
|
1740,
|
||||||
|
309
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
320.81564830322213,
|
||||||
|
46
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 11,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "samples",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vae",
|
||||||
|
"type": "VAE",
|
||||||
|
"link": 16,
|
||||||
|
"slot_index": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
27
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "VAEDecode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"type": "SaveImage",
|
||||||
|
"pos": [
|
||||||
|
1738,
|
||||||
|
413
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
317.3241088867178,
|
||||||
|
288.2958862304681
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 12,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 27
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {},
|
||||||
|
"widgets_values": [
|
||||||
|
"Avatar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
219,
|
||||||
|
301
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
398.1301946411129,
|
||||||
|
115.324566581726
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 6,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 11,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
23
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"looking at viewer, detailed face, open mouth, [smile], solo,eye-level angle, upper body, half body"
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
221,
|
||||||
|
469
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
395.5002463378903,
|
||||||
|
111.47454442596415
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 7,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 14,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
13
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"nsfw, worst quality, low quality, normal quality, monochrome, grayscale, closed mouth, extended tongue, sad, text, signature, username, artist name, animal, big mouth, leg, hand, missing body, ugly, blurry"
|
||||||
|
],
|
||||||
|
"color": "#322",
|
||||||
|
"bgcolor": "#533"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"type": "CheckpointLoaderSimple",
|
||||||
|
"pos": [
|
||||||
|
-150,
|
||||||
|
307
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
324.9937083129885,
|
||||||
|
98
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 4,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "MODEL",
|
||||||
|
"type": "MODEL",
|
||||||
|
"links": [
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CLIP",
|
||||||
|
"type": "CLIP",
|
||||||
|
"links": [
|
||||||
|
11,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VAE",
|
||||||
|
"type": "VAE",
|
||||||
|
"links": [
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CheckpointLoaderSimple"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"meinamix_meinaV10.safetensors"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"type": "LoadImage",
|
||||||
|
"pos": [
|
||||||
|
653,
|
||||||
|
461
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
314
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MASK",
|
||||||
|
"type": "MASK",
|
||||||
|
"links": null,
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "LoadImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"ComfyUI_temp_nycbp_00001_.png",
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "LoadImage",
|
||||||
|
"pos": [
|
||||||
|
1020,
|
||||||
|
468
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
314.00001525878906
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MASK",
|
||||||
|
"type": "MASK",
|
||||||
|
"links": null,
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "LoadImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"ComfyUI_temp_rlaph_00001_s.png",
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
1379,
|
||||||
|
305
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 315,
|
||||||
|
"1": 262
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 10,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 13,
|
||||||
|
"slot_index": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 18,
|
||||||
|
"slot_index": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
291874885197794,
|
||||||
|
"randomize",
|
||||||
|
25,
|
||||||
|
7,
|
||||||
|
"dpmpp_2m",
|
||||||
|
"karras",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "ControlNetLoader",
|
||||||
|
"pos": [
|
||||||
|
1020,
|
||||||
|
190
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 315,
|
||||||
|
"1": 58
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONTROL_NET",
|
||||||
|
"type": "CONTROL_NET",
|
||||||
|
"links": [
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "ControlNetLoader"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"control_v11p_sd15_normalbae_fp16.safetensors"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"type": "ControlNetLoader",
|
||||||
|
"pos": [
|
||||||
|
665,
|
||||||
|
190
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 315,
|
||||||
|
"1": 58
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONTROL_NET",
|
||||||
|
"type": "CONTROL_NET",
|
||||||
|
"links": [
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "ControlNetLoader"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"control_v11p_sd15_openpose_fp16.safetensors"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"type": "EmptyLatentImage",
|
||||||
|
"pos": [
|
||||||
|
1377,
|
||||||
|
150
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 315,
|
||||||
|
"1": 106
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
18
|
||||||
|
],
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "EmptyLatentImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
512,
|
||||||
|
768,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
7,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
7,
|
||||||
|
2,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
0,
|
||||||
|
7,
|
||||||
|
1,
|
||||||
|
"CONTROL_NET"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
9,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
9,
|
||||||
|
1,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
12,
|
||||||
|
11,
|
||||||
|
0,
|
||||||
|
9,
|
||||||
|
0,
|
||||||
|
"MODEL"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
13,
|
||||||
|
12,
|
||||||
|
0,
|
||||||
|
9,
|
||||||
|
2,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14,
|
||||||
|
11,
|
||||||
|
1,
|
||||||
|
12,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15,
|
||||||
|
9,
|
||||||
|
0,
|
||||||
|
13,
|
||||||
|
0,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16,
|
||||||
|
11,
|
||||||
|
2,
|
||||||
|
13,
|
||||||
|
1,
|
||||||
|
"VAE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18,
|
||||||
|
15,
|
||||||
|
0,
|
||||||
|
9,
|
||||||
|
3,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22,
|
||||||
|
20,
|
||||||
|
0,
|
||||||
|
19,
|
||||||
|
1,
|
||||||
|
"CONTROL_NET"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23,
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
19,
|
||||||
|
0,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24,
|
||||||
|
19,
|
||||||
|
0,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26,
|
||||||
|
22,
|
||||||
|
0,
|
||||||
|
19,
|
||||||
|
2,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27,
|
||||||
|
13,
|
||||||
|
0,
|
||||||
|
23,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user