Compare commits

...
192 Commits
Author SHA1 Message Date
shadowcz007 0846013378 增加 MiniCPM-V 2.6 int4 2024-08-22 14:46:11 +08:00
shadowcz007 c141ba405f fixbug: 自动监听文件夹 2024-08-20 15:06:58 +08:00
shadowcz007 0320f13a9f fixbug 2024-08-19 11:46:55 +08:00
shadowcz007 8adc34be4d fixbug 2024-08-19 10:29:54 +08:00
shadowcz007 cb6810d3c1 Update TextGenerateNode.py 2024-08-18 14:31:22 +08:00
shadowcz007 d384f64abf update :text-to-text 2024-08-17 22:15:31 +08:00
shadowcz007 ef7035f8ee update 2024-08-17 15:11:22 +08:00
shadowcz007 bfcadde5c3 update 2024-08-17 14:22:31 +08:00
shadowcz007 496ff41782 新ui支持,适配后,暂未全面测试 2024-08-16 18:13:05 +08:00
shadowcz007 46f0be5484 add image 2024-08-14 00:27:50 +08:00
shadowcz007 f3db0131c1 fixbug 2024-08-14 00:02:18 +08:00
shadowcz007 83a8d47f51 Update README.md 2024-08-13 23:32:10 +08:00
shadowcz007 fee0222910 v0.37.0 移动端适配、修改app模式的Mask编辑器 2024-08-12 10:10:43 +08:00
shadowcz007 1ed7b5511f mixlab app new mask editor 2024-08-12 00:19:26 +08:00
shadowcz007 b8f7c31537 Update index.html 2024-08-11 17:53:18 +08:00
shadowcz007 164791c257 webui 移动端适配 2024-08-11 17:20:36 +08:00
shadowcz007 8f5e599928 fixbug & ui 2024-08-11 16:29:16 +08:00
shadowcz007 7a7aaeb84d Update index.html 2024-08-10 23:00:58 +08:00
shadowcz007 e2136ab2fc fixbug 2024-08-10 17:13:28 +08:00
shadowcz007 c75cb21946 clean 2024-08-10 10:47:54 +08:00
shadowcz007 bf95218c91 p5-video-workflow 2024-08-10 00:59:44 +08:00
shadowcz007 8cb4507a5f v0.36.0 p5.js 2024-08-10 00:39:27 +08:00
shadowcz007 555890d1ba Update pyproject.toml 2024-08-09 19:08:36 +08:00
shadowcz007 e4f54e83b6 Update Text-to-Image-app.json 2024-08-09 16:30:47 +08:00
shadowcz007 692c4a709e fixbug:web app 2024-08-09 16:27:34 +08:00
shadowcz007 cbd1961459 test 2024-08-08 21:58:44 +08:00
shadowcz007 2e31a33ebf fixbug 2024-08-08 11:37:36 +08:00
shadowcz007 d16c6137d2 update 2024-08-06 23:07:20 +08:00
shadowcz007 0416ab79ec Update 3d_mixlab.js 2024-08-06 21:08:52 +08:00
shadow fc9a1c62b9 Merge pull request #295 from shadowcz007/0.36.0-py5-processing
Lama 改成手动安装,新增JsonRepair
2024-08-06 11:09:27 +08:00
shadowcz007 5d4567b134 Lama 改成手动安装,新增JsonRepair 2024-08-06 11:08:50 +08:00
shadow ae4a17d271 Merge pull request #293 from shadowcz007/0.36.0-py5-processing
0.36.0 py5 processing
2024-08-06 00:24:13 +08:00
shadowcz007 d110a08889 Update __init__.py 2024-08-06 00:23:34 +08:00
shadowcz007 e0157293cb Update P5.py 2024-08-06 00:21:51 +08:00
shadowcz007 0d985b3b65 update 2024-08-06 00:14:05 +08:00
shadowcz007 a65ade9fda updage 2024-08-05 21:30:30 +08:00
shadowcz007 874d6c8cb1 1 2024-08-05 21:16:19 +08:00
shadowcz007 f70ba2afa3 update 2024-08-05 21:08:32 +08:00
shadowcz007 e9f821e578 update 2024-08-05 20:49:06 +08:00
shadowcz007 8e488d4b1d update 2024-08-05 11:55:07 +08:00
shadowcz007 77201a457d 基本打通 2024-08-04 23:48:06 +08:00
shadowcz007 076e3b1178 test 2024-08-04 22:28:16 +08:00
shadowcz007 6b13fa64dc update 2024-08-04 20:44:56 +08:00
shadowcz007 846671a890 preview audio 2024-08-04 18:06:37 +08:00
shadowcz007 05b3088b75 0.35.1 2024-08-04 18:02:13 +08:00
shadowcz007 fe57286959 v0.34.0 2024-08-04 15:28:47 +08:00
shadowcz007 03645bbb33 image batch to list 2024-08-04 13:35:22 +08:00
shadowcz007 93dba9a399 fixbug :load image (base64) 2024-08-04 12:12:41 +08:00
shadowcz007 5627ea8073 Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-08-04 09:40:43 +08:00
shadowcz007 7ba679c9ce fixbug 2024-08-04 09:40:40 +08:00
shadow c7a450e6ce Merge pull request #289 from ComfyNodePRs/licence-update
Update PyProject Toml - License
2024-08-03 17:50:25 +08:00
snomiao beda5156bf chore(licence-update): Update PyProject Toml - License 2024-08-02 23:03:55 +00:00
shadowcz007 76a9da7163 fixbug 2024-08-02 18:32:27 +08:00
shadowcz007 edd0303f59 App模式增加batch prompt,批量提示词,可以把动态提示词批量组成后运行 2024-08-01 21:12:58 +08:00
shadowcz007 be6f47a333 batch prompt :批量提示 2024-08-01 21:03:40 +08:00
shadowcz007 4cd6a072ca Update install.bat 2024-08-01 11:58:23 +08:00
shadowcz007 743a82efe9 fixbug 2024-07-29 18:29:16 +08:00
shadowcz007 9589f28ef7 v0.32.0 2024-07-29 18:11:57 +08:00
shadowcz007 35492c5671 add SiliconflowLLM 2024-07-29 18:06:32 +08:00
shadow db1e695bf3 Merge pull request #284 from cd0304/main
修正text image节点的padding问题
2024-07-29 17:51:29 +08:00
shadowcz007 ecc4aec43b Update ChatGPT.py 2024-07-29 15:17:00 +08:00
shadowcz007 fc063c2205 Update __init__.py 2024-07-29 14:17:57 +08:00
shadowcz007 4d60ce138a Update __init__.py 2024-07-28 21:12:39 +08:00
shadowcz007 2afd24f6e4 fixbug 2024-07-28 20:52:55 +08:00
shadowcz007 437acd023a fixbug 2024-07-28 20:28:34 +08:00
shadowcz007 b00523ae14 优化mixlab app,前端不传workflow,只传输入和输出 2024-07-28 20:21:53 +08:00
shadowcz007 4405a74993 Update Audio.py 2024-07-26 18:56:38 +08:00
cd0304 cb16090868 Update ImageNode.py 2024-07-26 13:04:17 +08:00
cd0304 396e510dce Update ImageNode.py
fix height
2024-07-26 00:32:56 +08:00
shadowcz007 3b9790b969 Update __init__.py 2024-07-25 13:39:41 +08:00
shadowcz007 a35d07a7ac video 2024-07-17 20:49:15 +08:00
shadowcz007 6d004c61fc Update pyproject.toml 2024-07-17 14:41:33 +08:00
shadowcz007 ffdd06da1b Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-07-17 14:41:02 +08:00
shadowcz007 f03f34cacb Update checkVersion_mixlab.js 2024-07-17 14:40:59 +08:00
shadow 0c86ea849e Merge pull request #273 from cd0304/main
textimge节点增加对otf后缀字体支持
2024-07-17 14:37:35 +08:00
cd0304 0efa4c38c0 Update ImageNode.py 2024-07-17 13:59:22 +08:00
cd0304 6092ab7793 Update ImageNode.py 2024-07-17 13:17:40 +08:00
shadowcz007 929def87eb Update ui_mixlab.js 2024-07-17 11:16:43 +08:00
shadowcz007 be074ccff7 Update __init__.py 2024-07-16 22:47:10 +08:00
shadowcz007 3445199393 AUDIO 2024-07-16 21:38:54 +08:00
shadowcz007 216c7e152e 0.30.3 2024-07-08 00:03:33 +08:00
shadowcz007 cc8bc10690 update 2024-07-07 18:41:56 +08:00
shadowcz007 69b4218d60 Update __init__.py 2024-07-07 17:05:06 +08:00
shadowcz007 1dd18dc4f8 fixbug 2024-07-06 20:30:50 +08:00
shadowcz007 4ccbd999d9 fixbug 2024-07-06 00:54:19 +08:00
shadowcz007 fa8d404964 0.30.2 2024-07-06 00:39:02 +08:00
shadowcz007 30086957c9 fixbug 2024-07-06 00:37:52 +08:00
shadowcz007 0e57c620c9 Update Video.py 2024-07-04 18:19:53 +08:00
shadowcz007 3ce1c59a2d Update README.md 2024-07-04 17:37:50 +08:00
shadowcz007 3337e20b9e Math Operation 2024-06-23 16:50:06 +08:00
shadowcz007 e816b3626e update 2024-06-22 21:44:33 +08:00
shadowcz007 3e0cb0f17a Update ui_mixlab.js 2024-06-22 18:42:12 +08:00
shadowcz007 41bc606217 Update 2-screeshare.json 2024-06-22 11:56:32 +08:00
shadowcz007 5a5f4ca49a Update pyproject.toml 2024-06-21 23:08:27 +08:00
shadowcz007 c3a8437cd1 Update ImageNode.py 2024-06-21 22:10:19 +08:00
shadowcz007 8d8a1a392d fixbug 2024-06-21 21:54:41 +08:00
shadowcz007 5f93fb5e55 增加支持的国产大模型 2024-06-21 17:40:15 +08:00
shadowcz007 d05050d7d8 v0.30.1 2024-06-20 20:39:43 +08:00
shadowcz007 8e9744100d 优化composite images节点 2024-06-20 17:46:46 +08:00
shadowcz007 1e4e7e287d Update ImageNode.py 2024-06-20 16:33:58 +08:00
shadowcz007 e8f0c73f08 优化text image节点,更为精准控制空白间距,字体修改为选择方式 2024-06-20 16:32:04 +08:00
shadowcz007 e923e28f8d Canvas Mode 2024-06-20 14:59:51 +08:00
shadowcz007 5cc75bfa7c Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-06-20 12:04:29 +08:00
shadowcz007 d6701769b8 fixbug:showtext 2024-06-20 12:04:23 +08:00
shadow 0ddc67bdab Create CNAME 2024-06-20 11:13:12 +08:00
shadowcz007 38b62b7a68 Update pyproject.toml 2024-06-19 11:10:27 +08:00
shadowcz007 7e726000c7 v0.30.0 2024-06-18 16:57:14 +08:00
shadowcz007 d8dfb292ec 增加 Edit Mask & SD3 示例 2024-06-18 16:55:59 +08:00
shadowcz007 826975241d Audio Play 2024-06-17 10:50:26 +08:00
shadowcz007 743637ceaf Update Video.py 2024-06-14 11:50:05 +08:00
shadowcz007 e350c7e31e CombineAudioVideo、LoadAndCombinedAudio 2024-06-14 11:38:10 +08:00
shadowcz007 66b1e0ab9f Update __init__.py 2024-06-14 08:17:04 +08:00
shadowcz007 7b0374d110 Update requirements.txt 2024-06-13 09:04:48 +08:00
shadowcz007 e86ef8cbb0 ImageBatchToList、LoadAndCombinedAudio、combine_audio_video、GenerateFramesByCount 2024-06-12 20:54:16 +08:00
shadowcz007 8c901c54bc Update extension-node-map.json 2024-06-08 17:41:40 +08:00
shadowcz007 408d85691e v0.29.0 支持把输出显示到comfyui背景(TouchDesigner 风格) 2024-06-08 16:58:21 +08:00
shadowcz007 c66cd6901b appinfo add performance features
Appinfo supports outputting to the background, enhancing the performance features of ComfyUI.
2024-06-08 16:03:10 +08:00
shadowcz007 aeadbc4f6d fixbug 2024-06-06 15:17:40 +08:00
shadowcz007 224136890e fixbug 2024-06-06 08:02:51 +08:00
shadowcz007 3669a1e86d 0.28.3 2024-06-01 23:21:33 +08:00
shadowcz007 d588b5b327 Update index.html 2024-05-29 22:37:52 +08:00
shadowcz007 b705679098 Update index.html 2024-05-29 21:49:32 +08:00
shadowcz007 f71a0b0da5 Update index.html 2024-05-29 20:20:19 +08:00
shadowcz007 ebc2c76b6b fixbug 2024-05-25 22:50:19 +08:00
shadow 2e3fff278e Merge pull request #240 from audioscavenger/patch-1
Update extension-node-map.json
2024-05-24 11:12:17 +08:00
Eric 1f4bc5e089 Update extension-node-map.json
i'm the new maintainer, thanks
2024-05-23 16:41:33 -07:00
shadowcz007 52c38b10dd v0.28.2 2024-05-23 18:19:30 +08:00
shadowcz007 7047aa5456 add video format 2024-05-23 16:59:04 +08:00
shadowcz007 33fe4019f7 Update ui_mixlab.js 2024-05-23 16:43:50 +08:00
shadowcz007 80b9d97690 Update Video.py 2024-05-23 15:58:24 +08:00
shadowcz007 3c3c92723f Update pyproject.toml 2024-05-23 10:34:36 +08:00
shadowcz007 037bd87006 Update pyproject.toml 2024-05-23 10:26:48 +08:00
shadowcz007 f688310d28 Update Utils.py 2024-05-23 10:14:25 +08:00
shadow c4d65e7a45 Merge pull request #234 from haohaocreates/publish
Add Github Action for Publishing to Comfy Registry
2024-05-22 23:14:33 +08:00
shadow 6f208b710d Merge pull request #235 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-05-22 23:14:17 +08:00
haohaocreates b599faaf85 Update pyproject.toml desc 2024-05-21 15:24:07 -04:00
haohaocreates 6d991d20dc chore(publish): Add Github Action for Publishing to Comfy Registry 2024-05-21 19:19:01 +00:00
haohaocreates c87e0296f6 chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-21 19:19:01 +00:00
shadow 16cdb4c5b4 Merge pull request #231 from 295958090/main
修复FloatSlider的bug
2024-05-21 21:57:10 +08:00
Bai Shui 7631b8924d 修复bug 2024-05-21 13:34:29 +08:00
shadowcz007 785d307ff3 Update index.html 2024-05-18 16:13:30 +08:00
shadowcz007 8c713ff35e Update index.html 2024-05-18 16:08:25 +08:00
shadowcz007 7d80493bef Update index.html 2024-05-18 16:05:39 +08:00
shadowcz007 bff2760c3d v0.28.1
修复bug
2024-05-18 11:38:50 +08:00
shadowcz007 a0f8848367 修复 当上传新的图片,编辑mask的bug 2024-05-18 11:38:28 +08:00
shadowcz007 5b1cbcd8d5 修复bug 2024-05-16 13:20:06 +08:00
shadowcz007 05857a92d5 v0.28.0
add rembg api & webapp rembg
2024-05-16 11:49:45 +08:00
shadowcz007 6bdc811286 add rembg api & webapp rembg 2024-05-16 11:49:14 +08:00
shadowcz007 469d50a5b8 Update index.html 2024-05-16 09:02:13 +08:00
shadowcz007 ef86904bfb Update ui_mixlab.js 2024-05-16 09:02:08 +08:00
shadowcz007 d4181ea67c v0.27.1 fixbug 2024-05-16 08:49:30 +08:00
shadowcz007 1c6d17309f Update index.html 2024-05-16 08:49:09 +08:00
shadowcz007 db293ec41d fixbug css 2024-05-16 08:47:17 +08:00
shadowcz007 db8d468f29 0.27.0 增加webapp的mask绘制 2024-05-16 00:11:16 +08:00
shadowcz007 d7d7af7265 add mask edit for webapp 2024-05-16 00:06:42 +08:00
shadowcz007 bd763cadc1 fixbug 2024-05-16 00:05:24 +08:00
shadowcz007 22799fc549 fixbug for mask 2024-05-16 00:05:16 +08:00
shadowcz007 0f231d1271 add minPaint for mask 2024-05-16 00:04:54 +08:00
shadowcz007 8c0c911020 Create LICENSE 2024-05-14 10:02:46 +08:00
shadowcz007 6cb9df700b 增加ComparingTwoFrames、右键image-to-text 2024-05-14 09:59:39 +08:00
shadowcz007 4fdda537b9 Update README.md 2024-05-14 09:57:51 +08:00
shadowcz007 cb6f32465a Update README.md 2024-05-14 09:55:34 +08:00
shadowcz007 c235e36cb4 Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-05-14 09:54:27 +08:00
shadowcz007 aaca440a94 Update README.md 2024-05-14 09:54:24 +08:00
shadow 1f57950a29 Update README.md 2024-05-14 09:39:57 +08:00
shadow 3d8855ec72 Update README.md 2024-05-14 09:39:45 +08:00
shadowcz007 ac9231d9f3 Update image_mixlab.js 2024-05-13 21:43:20 +08:00
shadowcz007 37c0a56d89 add ComparingTwoFrames 2024-05-13 21:33:44 +08:00
shadowcz007 010915dac4 add help 2024-05-13 11:37:17 +08:00
shadowcz007 83a8b3b970 Update ui_mixlab.js 2024-05-13 10:58:36 +08:00
shadowcz007 f130202aa1 resizeImage 2024-05-12 22:25:08 +08:00
shadowcz007 c8f6800bcd add image-to-text :llava-phi-3-mini-gguf 2024-05-12 17:34:53 +08:00
shadowcz007 078f9f5dd4 Update ui_mixlab.js 2024-05-12 00:03:11 +08:00
shadowcz007 c0de178c7d add re_start 2024-05-11 23:58:05 +08:00
shadowcz007 1c767b538d set n_gpu_layers 2024-05-11 17:53:18 +08:00
shadowcz007 51aab44b5d Update ui_mixlab.js 2024-05-11 14:43:35 +08:00
shadowcz007 b8a0d4a67b Update ui_mixlab.js 2024-05-11 14:17:01 +08:00
shadowcz007 cd0dcfbb8c v0.25.1 2024-05-11 14:12:00 +08:00
shadow fcc9e30eae Update __init__.py 2024-05-11 12:55:14 +08:00
shadowcz007 5f66218a43 修复 sys.stdout.isatty() object has no attribute 'isatty' 2024-05-11 12:28:14 +08:00
shadowcz007 61ef4f9a0f Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-05-11 12:13:34 +08:00
shadowcz007 f0a8734b42 修复 sys.stdout.isatty() object has no attribute 'isatty' 2024-05-11 12:13:31 +08:00
shadow 4fe95ef4ec Update README.md 2024-05-11 08:53:33 +08:00
shadowcz007 2a5148845b yaml 2024-05-10 14:38:32 +08:00
shadowcz007 8fa562caaf Update install.bat 2024-05-09 09:55:34 +08:00
shadowcz007 cab5620cd5 Update index.html 2024-05-08 23:42:48 +08:00
shadowcz007 be38d36677 Update index.html 2024-05-08 23:32:20 +08:00
shadowcz007 69236fca89 Update __init__.py 2024-05-08 22:55:42 +08:00
shadowcz007 4a4f376bfd Update __init__.py 2024-05-08 22:53:39 +08:00
shadowcz007 fd9718fe24 Update __init__.py 2024-05-08 22:50:30 +08:00
shadowcz007 26a6e11212 llama_cpp 2024-05-08 22:42:34 +08:00
shadowcz007 de1a669f6e Update README.md 2024-05-08 10:11:16 +08:00
103 changed files with 137170 additions and 5290 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+1
View File
@@ -0,0 +1 @@
mixlabnodes.com
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 shadow
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+138 -80
View File
@@ -1,8 +1,53 @@
> 适配了最新版comfyui的py3.11 ,torch 2.1.2+cu121
![](https://img.shields.io/github/release/shadowcz007/comfyui-mixlab-nodes)
> 适配了最新版 comfyui 的 py3.11 ,torch 2.3.1+cu121
> [Mixlab nodes discord](https://discord.gg/cXs9vZSqeK)
商务合作请联系 389570357@qq.com
For business cooperation, please contact email 389570357@qq.com
##### `最新`:
- 增加 MiniCPM-V 2.6 int4
This is the int4 quantized version of MiniCPM-V 2.6.
Running with int4 version would use lower GPU memory (about 7GB).
- 移动端适配、修改 app 模式的 Mask 编辑器
- 增加 p5.js 作为输入节点
[workflow](./workflow/p5workflow.json)
[workflow2](./workflow/p5-video-workflow.json)
- App 模式增加 batch prompt,批量提示词,可以把动态提示词批量组成后运行
![alt text](./assets/1722517810720.png)
- 增加 API Key Input 节点,用于管理 LLM 的 Key,同时优化 LLM 相关节点,为后续 agent 模式做准备
- 增加 SiliconflowLLM,可以使用由 Siliconflow 提供的免费 LLM
<!-- - ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/` -->
<!-- - 右键菜单支持 text-to-text,方便对 prompt 词补全 -->
<!--
强烈推荐:
[Phi-3-mini-4k-instruct-function-calling-GGUF](https://huggingface.co/nold/Phi-3-mini-4k-instruct-function-calling-GGUF)
[Phi-3-mini-4k-instruct-GGUF](https://huggingface.co/lmstudio-community/Phi-3-mini-4k-instruct-GGUF/tree/main),备选:[llama3_if_ai_sdpromptmkr_q2k](https://hf-mirror.com/impactframes/llama3_if_ai_sdpromptmkr_q2k/tree/main)
- 右键菜单支持 image-to-text,使用多模态模型,多模态使用 [llava-phi-3-mini-gguf](https://huggingface.co/xtuner/llava-phi-3-mini-gguf/tree/main),注意需要把llava-phi-3-mini-mmproj-f16.gguf也下载
![](./assets/prompt_ai_setup.png)
![](./assets/prompt-ai.png) -->
#### `相关插件推荐`
[comfyui-sd-prompt-mixlab](https://github.com/shadowcz007/comfyui-sd-prompt-mixlab)
[comfyui-liveportrait](https://github.com/shadowcz007/comfyui-liveportrait)
[Comfyui-ChatTTS](https://github.com/shadowcz007/Comfyui-ChatTTS)
[comfyui-sound-lab](https://github.com/shadowcz007/comfyui-sound-lab)
[comfyui-Image-reward](https://github.com/shadowcz007/comfyui-Image-reward)
@@ -12,27 +57,15 @@
<!-- [comfyui-CLIPSeg](https://github.com/shadowcz007/comfyui-CLIPSeg) -->
##### `最新`:
ChatGPT节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。
## 🚀🚗🚚🏃 Workflow-to-APP
Model download,move to :```models/llamafile/```
强烈推荐:[Phi-3-mini-4k-instruct-GGUF](https://huggingface.co/lmstudio-community/Phi-3-mini-4k-instruct-GGUF/tree/main)
备选:[llama3_if_ai_sdpromptmkr_q2k](https://hf-mirror.com/impactframes/llama3_if_ai_sdpromptmkr_q2k/tree/main)
> 右键菜单支持 text-to-text,方便对prompt词补全
![](./assets/prompt_ai_setup.png)
![](./assets/prompt-ai.png)
## 🚀🚗🚚🏃 Workflow-to-APP
- 新增AppInfo节点,可以通过简单的配置,把workflow转变为一个Web APP。
- 支持多个web app 切换
- 发布为app的workflow,可以在右键里再次编辑了
- web app可以设置分类,在comfyui右键菜单可以编辑更新web app
- 新增 AppInfo 节点,可以通过简单的配置,把 workflow 转变为一个 Web APP。
- 支持多个 web app 切换
- 发布为 app 的 workflow,可以在右键里再次编辑了
- web app 可以设置分类,在 comfyui 右键菜单可以编辑更新 web app
- 支持动态提示
- 支持把输出显示到 comfyui 背景(TouchDesigner 风格)
- 如果转为 web app 打开是空白的,注意检查下插件目录的名字需要是:comfyui-mixlab-nodes(如果是 zip 包下载会多了个-main 的后缀,需要去掉)
![](./assets/微信图片_20240421205440.png)
@@ -41,7 +74,6 @@ Model download,move to :```models/llamafile/```
- The workflow, which is now released as an app, can also be edited again by right-clicking.
- The web app can be configured with categories, and the web app can be edited and updated in the right-click menu of ComfyUI.
![](./assets/0-m-app.png)
![](./assets/appinfo-readme.png)
@@ -49,66 +81,72 @@ Model download,move to :```models/llamafile/```
![](./assets/appinfo-2.png)
Example:
- workflow
![APP info](./workflow/appinfo-workflow.svg)
[text-to-image](./workflow/Text-to-Image-app.json)
![APP info](./workflow/appinfo-workflow.svg)
[text-to-image](./workflow/Text-to-Image-app.json)
APP-JSON:
- [text-to-image](./example/Text-to-Image_3.json)
- [image-to-image](./example/Image-to-Image_2.json)
- text-to-text
> 暂时支持 9 种节点作为界面上的输入节点:Load Image、VHS_LoadVideo、CLIPTextEncode、PromptSlide、TextInput_、Color、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
> 暂时支持 9 种节点作为界面上的输入节点:Load Image、VHS*LoadVideo、CLIPTextEncode、PromptSlide、TextInput*、Color、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT、VHS_VideoCombine、PromptImage
> seed统一输入控件,支持:SamplerCustom、KSampler
> seed 统一输入控件,支持:SamplerCustom、KSampler
> 配套[ps插件](https://github.com/shadowcz007/comfyui-ps-plugin)
> 配套[ps 插件](https://github.com/shadowcz007/comfyui-ps-plugin)
> 如果遇到上传图片不成功,请检查下:局域网或者是云服务,请使用https,端口8189这个服务( 感谢 @Damien 反馈问题)
> 如果遇到上传图片不成功,请检查下:局域网或者是云服务,请使用 https,端口 8189 这个服务( 感谢 @Damien 反馈问题)
> If you encounter difficulties in uploading images, please check the following: for local network or cloud services, please use HTTPS and the service on port 8189. (Thanks to @Damien for reporting the issue.)
## 🏃🚗🚚🚀 Real-time Design
## 🏃🚗🚚🚀 Real-time Design
> ScreenShareNode & FloatingVideoNode. Now comfyui supports capturing screen pixel streams from any software and can be used for LCM-Lora integration. Let's get started with implementation and design! 💻🌐
![screenshare](./assets/screenshare.png)
https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43e-410a-ab3a-1952b7b4e7da
<!-- [ScreenShareNode](./workflow/2-screeshare.json) -->
[ScreenShareNode & FloatingVideoNode](./workflow/3-FloatVideo-workflow.json)
!! Please use the address with HTTPS (https://127.0.0.1).
### SpeechRecognition & SpeechSynthesis
![f](./assets/audio-workflow.svg)
[Voice + Real-time Face Swap Workflow](./workflow/语音+实时换脸workflow.json)
- Preview Audio
[text-to-audio](./workflow/text-to-audio-base-workflow.json)
### GPT
> Support for calling multiple GPTs.Local LLM(llama.cpp)、 ChatGPT、ChatGLM3 、ChatGLM4 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
![gpt-workflow.svg](./assets/gpt-workflow.svg)
> Support for calling multiple GPTs.Local LLM 、 ChatGPT、ChatGLM3 、ChatGLM4 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
[workflow-5](./workflow/5-gpt-workflow.json)
[LLM_base_workflow](./workflow/LLM_base_workflow.json)
- SiliconflowLLM
- ChatGPTOpenAI
最新:ChatGPT节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。
<!-- 最新:ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。
Model download,move to :```models/llamafile/```
Model download,move to :`models/llamafile/`
强烈推荐:[Phi-3-mini-4k-instruct-GGUF](https://huggingface.co/lmstudio-community/Phi-3-mini-4k-instruct-GGUF/tree/main)
备选:[llama3_if_ai_sdpromptmkr_q2k](https://hf-mirror.com/impactframes/llama3_if_ai_sdpromptmkr_q2k/tree/main)
> 如果碰到安装失败,可以尝试手动安装
```
../../../python_embeded/python.exe -s -m pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121
@@ -116,10 +154,23 @@ Model download,move to :```models/llamafile/```
```
> [Mac](https://llama-cpp-python.readthedocs.io/en/latest/install/macos/)
```
pip uninstall llama-cpp-python -y
CMAKE_ARGS="-DLLAMA_METAL=on" pip install -U llama-cpp-python --no-cache-dir
pip install 'llama-cpp-python[server]'
```
```
pip install llama-cpp-python \
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/metal
``` -->
## Prompt
> PromptSlide
![](./assets/prompt_weight.png)
> ![](./assets/prompt_weight.png)
<!-- ![](./workflow/promptslide-appinfo-workflow.svg) -->
@@ -133,20 +184,22 @@ Model download,move to :```models/llamafile/```
> PromptImage & PromptSimplification,Assist in simplifying prompt words, comparing images and prompt word nodes.
> ChinesePrompt && PromptGenerate,中文prompt节点,直接用中文书写你的prompt
> ChinesePrompt && PromptGenerate,中文 prompt 节点,直接用中文书写你的 prompt
![](./assets/ChinesePrompt_workflow.svg)
### Layers
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
> The composite images node overlays a foreground image onto a background image at specified positions and scales, with optional blending modes and masking capabilities. position : 'overall',"center_center","left_bottom","center_bottom","right_bottom","left_top","center_top","right_top"
![layers](./assets/layers-workflow.svg)
![poster](./assets/poster-workflow.svg)
### 3D
![](./assets/3d-workflow.png)
![](./assets/3d_app.png)
[workflow](./assets/Image-to-3D_1.json)
@@ -154,78 +207,91 @@ Model download,move to :```models/llamafile/```
![](./assets/3dimage.png)
[workflow](./workflow/3D-workflow.json)
### Image
#### LoadImagesToBatch
> Upload multiple images for batch input into the IP adapter.
> Upload multiple images for batch input into the IP adapter.
#### LoadImagesFromLocal
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop.
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop.
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
#### LoadImagesFromURL
> Conveniently load images from a fixed address on the internet to ensure that default images in the workflow can be executed.
#### TextImage
> [下载字体](https://drxie.github.io/OSFCC/)放到 `custom_nodes/comfyui-mixlab-nodes/assets/fonts`
#### MiniCPM-VQA Simple
This is the int4 quantized version of MiniCPM-V 2.6.
Running with int4 version would use lower GPU memory (about 7GB).
[模型](https://huggingface.co/openbmb/MiniCPM-V-2_6-int4)
![alt text](assets/1724308322276.png)
### Style
> Apply VisualStyle Prompting , Modified from [ComfyUI_VisualStylePrompting](https://github.com/ExponentialML/ComfyUI_VisualStylePrompting)
> Apply VisualStyle Prompting , Modified from [ComfyUI_VisualStylePrompting](https://github.com/ExponentialML/ComfyUI_VisualStylePrompting)
![](./assets/VisualStylePrompting.png)
> StyleAligned , Modified from [style_aligned_comfy](https://github.com/brianfitzgerald/style_aligned_comfy)
> StyleAligned , Modified from [style_aligned_comfy](https://github.com/brianfitzgerald/style_aligned_comfy)
### Utils
> The Color node provides a color picker for easy color selection, the Font node offers built-in font selection for use with TextImage to generate text images, and the DynamicDelayByText node allows delayed execution based on the length of the input text.
- [添加了DynamicDelayByText功能,可以根据输入文本的长度进行延迟执行。](./workflow/audio-chatgpt-workflow.json)
- [添加了 DynamicDelayByText 功能,可以根据输入文本的长度进行延迟执行。](./workflow/audio-chatgpt-workflow.json)
- [Added DynamicDelayByText, enabling delayed execution based on input text length.](./workflow/audio-chatgpt-workflow.json)
- [使用CkptNames 对比不同的模型效果](./workflow/ckpts-image-workflow.json)
- [使用 CkptNames 对比不同的模型效果](./workflow/ckpts-image-workflow.json)
- [CkptNames compare the effects of different models.](./workflow/ckpts-image-workflow.json)
### Other Nodes
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
![main](./assets/all-workflow.svg)
![main2](./assets/detect-face-all.png)
[workflow-1](./workflow/1-workflow.json)
> TransparentImage
![TransparentImage](./assets/TransparentImage.png)
> FeatheredMask、SmoothMask
Add edges to an image.
![FeatheredMask](./assets/FlVou_Y6kaGWYoEj1Tn0aTd4AjMI.jpg)
> LaMaInpainting(需要手动安装)
> LaMaInpainting
- simple-lama-inpainting 里的 pillow 造成冲突,暂时从依赖里移除,如果有安装 simple-lama-inpainting ,节点会自动添加,没有,则不会自动添加。
from [simple-lama-inpainting](https://github.com/enesmsahin/simple-lama-inpainting)
- [问题汇总](https://github.com/shadowcz007/comfyui-mixlab-nodes/issues/294)
> rembgNode
"briarmbg","u2net","u2netp","u2net_human_seg","u2net_cloth_seg","silueta","isnet-general-use","isnet-anime"
*** briarmbg *** model was developed by BRlA Al and can be used as an open-source model for non-commercial purposes
**_ briarmbg _** model was developed by BRlA Al and can be used as an open-source model for non-commercial purposes
### Improvement
### Improvement
- Add "help" option to the context menu for each node.
- Add "Nodes Map" option to the global context menu.
@@ -236,23 +302,21 @@ An improvement has been made to directly redirect to GitHub to search for missin
![node-not-found](./assets/node-not-found.png)
### Models
* [Download TripoSR](https://huggingface.co/stabilityai/TripoSR/blob/main/model.ckpt) and place it in ```models/triposr```
- [Download TripoSR](https://huggingface.co/stabilityai/TripoSR/blob/main/model.ckpt) and place it in `models/triposr`
* [Download facebook/dino-vitb16](https://huggingface.co/facebook/dino-vitb16/tree/main) and place it in ```models/triposr/facebook/dino-vitb16```
- [Download facebook/dino-vitb16](https://huggingface.co/facebook/dino-vitb16/tree/main) and place it in `models/triposr/facebook/dino-vitb16`
[Download rembg Models](https://github.com/danielgatis/rembg/tree/main#Models),move to:`models/rembg`
[Download rembg Models](https://github.com/danielgatis/rembg/tree/main#Models),move to:```models/rembg```
[Download lama](https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt), move to : `models/lama`
[Download lama](https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt), move to : ```models/lama```
[Download Salesforce/blip-image-captioning-base](https://huggingface.co/Salesforce/blip-image-captioning-base), move to :`models/clip_interrogator/Salesforce/blip-image-captioning-base`
[Download Salesforce/blip-image-captioning-base](https://huggingface.co/Salesforce/blip-image-captioning-base), move to :```models/clip_interrogator/Salesforce/blip-image-captioning-base```
[Download succinctly/text2image-prompt-generator](https://huggingface.co/succinctly/text2image-prompt-generator/tree/main),move to:`models/prompt_generator/text2image-prompt-generator`
[Download succinctly/text2image-prompt-generator](https://huggingface.co/succinctly/text2image-prompt-generator/tree/main),move to:```models/prompt_generator/text2image-prompt-generator```
[Download Helsinki-NLP/opus-mt-zh-en](https://huggingface.co/Helsinki-NLP/opus-mt-zh-en/tree/main),move to:```models/prompt_generator/opus-mt-zh-en```
[Download Helsinki-NLP/opus-mt-zh-en](https://huggingface.co/Helsinki-NLP/opus-mt-zh-en/tree/main),move to:`models/prompt_generator/opus-mt-zh-en`
## Installation
@@ -268,40 +332,35 @@ git clone https://github.com/shadowcz007/comfyui-mixlab-nodes.git
Install the requirements:
run directly:
```
cd ComfyUI/custom_nodes/comfyui-mixlab-nodes
install.bat
```
or install the requirements using:
```
../../../python_embeded/python.exe -s -m pip install -r requirements.txt
```
If you are using a venv, make sure you have it activated before installation and use:
```
pip3 install -r requirements.txt
```
#### Chinese community
访问 [www.mixcomfy.com](https://www.mixcomfy.com),获得更多内测功能,关注微信公众号:Mixlab无界社区
访问 [www.mixcomfy.com](https://www.mixcomfy.com),获得更多内测功能,关注微信公众号:Mixlab 无界社区
####
####
File / LoadImagesFromPath SaveImageToLocal LoadImagesFromURL
#### discussions:
[discussions](https://github.com/shadowcz007/comfyui-mixlab-nodes/discussions)
[discussions](https://github.com/shadowcz007/comfyui-mixlab-nodes/discussions)
<picture>
<source
@@ -321,4 +380,3 @@ File / LoadImagesFromPath SaveImageToLocal LoadImagesFromURL
src="https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date"
/>
</picture>
+650 -168
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.
+12876 -554
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,9 +11,9 @@ if exist "%python_exec%" (
%python_exec% -s -m pip install "%%i" -i https://pypi.tuna.tsinghua.edu.cn/simple
)
%python_exec% -s -m pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121
@REM %python_exec% -s -m pip install --upgrade --force llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121
%python_exec% -s -m pip install llama-cpp-python[server]
@REM %python_exec% -s -m pip install --upgrade --force llama-cpp-python[server]
) else (
+57 -37
View File
@@ -1,6 +1,7 @@
import os
import folder_paths
import torchaudio
class SpeechRecognition:
@classmethod
@@ -55,46 +56,65 @@ class SpeechSynthesis:
return {"ui": {"text": text}, "result": (text,)}
#
class GamePal:
class AudioPlayNode:
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = ""
self.compress_level = 4
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_text": ("STRING",{"multiline": True,"default": ""}),
},
"optional": {
"input_num": ("INT",{
"default":100,
"min": -1, #Minimum value
"max": 0xffffffffffffffff, #Maximum value
"step": 1, #Slider's step
"display": "slider" # Cosmetic only: display as "number" or "slider"
}),
"python_code": ("STRING",{"multiline": True,"default": "result= 1 if 'Mixlab' in input_text else 0"}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("INT",)
return {"required": {
"audio": ("AUDIO",),
},
}
RETURN_TYPES = ()
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (False,)
CATEGORY = "♾️Mixlab/Audio"
def run(self, input_text,input_num,python_code):
exec(python_code)
res=None
try:
# 可能会引发异常的代码
res=result
except:
# 处理异常的代码
print('')
INPUT_IS_LIST = False
OUTPUT_IS_LIST = ()
print(res)
OUTPUT_NODE = True
def run(self,audio):
# print(session_history)
return {"ui": {"text": [input_text],"num":[input_num]}, "result": (res,)}
# 判断是否是 Tensor 类型
is_tensor = not isinstance(audio, dict)
# print('#判断是否是 Tensor 类型',is_tensor,audio)
if not is_tensor and 'waveform' in audio and 'sample_rate' in audio:
# {'waveform': tensor([], size=(1, 1, 0)), 'sample_rate': 44100}
is_tensor=True
if is_tensor and (not 'audio_path' in audio):
filename_prefix=""
# 保存
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
results = list()
filename_with_batch_num = filename.replace("%batch_num%", str(1))
file = f"{filename_with_batch_num}_{counter:05}_.wav"
torchaudio.save(os.path.join(full_output_folder, file), audio['waveform'].squeeze(0), audio["sample_rate"])
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
else:
results=[{
"filename": audio['filename'],
"subfolder":audio['subfolder'],
"type": audio['type'],
"audio_path":audio['audio_path']
}]
# print(audio)
return {"ui": {"audio":results}}
+336 -96
View File
@@ -6,14 +6,69 @@ import folder_paths
import hashlib
import codecs,sys
import importlib.util
import subprocess
python = sys.executable
# 从文本中提取json
def extract_json_strings(text):
json_strings = []
brace_level = 0
json_str = ''
in_json = False
for char in text:
if char == '{':
brace_level += 1
in_json = True
if in_json:
json_str += char
if char == '}':
brace_level -= 1
if in_json and brace_level == 0:
json_strings.append(json_str)
json_str = ''
in_json = False
return json_strings[0] if len(json_strings)>0 else "{}"
def is_installed(package):
def is_installed(package, package_overwrite=None,auto_install=True):
is_has=False
try:
spec = importlib.util.find_spec(package)
is_has=spec is not None
except ModuleNotFoundError:
return False
return spec is not None
pass
package = package_overwrite or package
if spec is None:
if auto_install==True:
print(f"Installing {package}...")
# 清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple
command = f'"{python}" -m pip install {package}'
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=os.environ)
is_has=True
if result.returncode != 0:
print(f"Couldn't install\nCommand: {command}\nError code: {result.returncode}")
is_has=False
else:
print(package+'## OK')
return is_has
# def is_installed(package):
# try:
# spec = importlib.util.find_spec(package)
# except ModuleNotFoundError:
# return False
# return spec is not None
def get_unique_hash(string):
@@ -53,30 +108,14 @@ def azure_client(key,url):
def openai_client(key,url):
client = openai.OpenAI(
api_key=key,
base_url=url
api_key=key,
base_url=url
)
return client
def ZhipuAI_client(key):
try:
if is_installed('zhipuai')==False:
import subprocess
# 安装
print('#pip install zhipuai')
result = subprocess.run([sys.executable, '-s', '-m', 'pip', 'install', 'zhipuai'], capture_output=True, text=True)
#检查命令执行结果
if result.returncode == 0:
print("#install success")
from zhipuai import ZhipuAI
else:
print("#install error")
else:
if is_installed('zhipuai')==True:
from zhipuai import ZhipuAI
except:
print("#install zhipuai error")
@@ -97,73 +136,76 @@ def get_llama_path():
except:
return os.path.join(folder_paths.models_dir, "llamafile")
def get_llama_models():
res=[]
# def get_llama_models():
# res=[]
model_path=get_llama_path()
if os.path.exists(model_path):
files = os.listdir(model_path)
for file in files:
if os.path.isfile(os.path.join(model_path, file)):
res.append(file)
res=phi_sort(res)
return res
# model_path=get_llama_path()
# if os.path.exists(model_path):
# files = os.listdir(model_path)
# for file in files:
# if os.path.isfile(os.path.join(model_path, file)):
# res.append(file)
# res=phi_sort(res)
# return res
llama_modes_list=get_llama_models()
# llama_modes_list=get_llama_models()
# llama_modes_list=[]
def get_llama_model_path(file_name):
model_path=get_llama_path()
mp=os.path.join(model_path,file_name)
return mp
# def get_llama_model_path(file_name):
# model_path=get_llama_path()
# mp=os.path.join(model_path,file_name)
# return mp
def llama_cpp_client(file_name):
try:
if is_installed('llama_cpp')==False:
import subprocess
# def llama_cpp_client(file_name):
# try:
# if is_installed('llama_cpp')==False:
# import subprocess
# 安装
print('#pip install llama-cpp-python')
# # 安装
# print('#pip install llama-cpp-python')
result = subprocess.run([sys.executable, '-s', '-m', 'pip',
'install',
'llama-cpp-python',
'--extra-index-url',
'https://abetlen.github.io/llama-cpp-python/whl/cu121'
], capture_output=True, text=True)
# result = subprocess.run([sys.executable, '-s', '-m', 'pip',
# 'install',
# 'llama-cpp-python',
# '--extra-index-url',
# 'https://abetlen.github.io/llama-cpp-python/whl/cu121'
# ], capture_output=True, text=True)
#检查命令执行结果
if result.returncode == 0:
print("#install success")
from llama_cpp import Llama
# #检查命令执行结果
# if result.returncode == 0:
# print("#install success")
# from llama_cpp import Llama
subprocess.run([sys.executable, '-s', '-m', 'pip',
'install',
'llama-cpp-python[server]'
], capture_output=True, text=True)
# subprocess.run([sys.executable, '-s', '-m', 'pip',
# 'install',
# 'llama-cpp-python[server]'
# ], capture_output=True, text=True)
else:
print("#install error")
# else:
# print("#install error")
else:
from llama_cpp import Llama
except:
print("#install llama-cpp-python error")
# else:
# from llama_cpp import Llama
# except:
# print("#install llama-cpp-python error")
if file_name:
mp=get_llama_model_path(file_name)
# file_name=get_llama_models()[0]
# model_path=os.path.join(folder_paths.models_dir, "llamafile")
# mp=os.path.join(model_path,file_name)
# if file_name:
# mp=get_llama_model_path(file_name)
# # file_name=get_llama_models()[0]
# # model_path=os.path.join(folder_paths.models_dir, "llamafile")
# # mp=os.path.join(model_path,file_name)
llm = Llama(model_path=mp, chat_format="chatml",n_gpu_layers=-1,n_ctx=512)
# llm = Llama(model_path=mp, chat_format="chatml",n_gpu_layers=-1,n_ctx=512)
return llm
# return llm
if is_installed('json_repair'):
from json_repair import repair_json
def chat(client, model_name,messages ):
print('#chat',model_name,messages)
try_count = 0
while True:
try_count += 1
@@ -206,6 +248,36 @@ def chat(client, model_name,messages ):
return content
llm_apis=[
{
"value": "https://api.openai.com/v1",
"label": "openai"
},
{
"value": "https://openai.api2d.net/v1",
"label": "api2d"
},
# {
# "value": "https://docs-test-001.openai.azure.com",
# "label": "https://docs-test-001.openai.azure.com"
# },
{
"value": "https://api.moonshot.cn/v1",
"label": "Kimi"
},
{
"value": "https://api.deepseek.com/v1",
"label": "DeepSeek-V2"
},
{
"value": "https://api.siliconflow.cn/v1",
"label": "SiliconCloud"
}]
llm_apis_dict = {api["label"]: api["value"] for api in llm_apis}
class ChatGPTNode:
def __init__(self):
# self.__client = OpenAI()
@@ -215,35 +287,60 @@ class ChatGPTNode:
@classmethod
def INPUT_TYPES(cls):
model_list=llama_modes_list+[
"gpt-3.5-turbo",
"gpt-3.5-turbo-0125",
"gpt-35-turbo",
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-16k-0613",
"gpt-4-0613",
"gpt-4-1106-preview",
"glm-4"
model_list=[
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4",
"gpt-4-0314",
"gpt-4-0613",
"gpt-3.5-turbo-0301",
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"qwen-turbo",
"qwen-plus",
"qwen-long",
"qwen-max",
"qwen-max-longcontext",
"glm-4",
"glm-3-turbo",
"moonshot-v1-8k",
"moonshot-v1-32k",
"moonshot-v1-128k",
"deepseek-chat",
"Qwen/Qwen2-7B-Instruct",
"THUDM/glm-4-9b-chat",
"01-ai/Yi-1.5-9B-Chat-16K",
"meta-llama/Meta-Llama-3.1-8B-Instruct"
]
return {
"required": {
"api_key":("KEY", {"default": "", "multiline": True,"dynamicPrompts": False}),
"api_url":("URL", {"default": "", "multiline": True,"dynamicPrompts": False}),
# "api_key":("KEY", {"default": "", "multiline": True,"dynamicPrompts": False}),
# "api_key":("STRING", {"forceInput": True,}),
"prompt": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"system_content": ("STRING",
{
"default": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"multiline": True,"dynamicPrompts": False
}),
"model": ( model_list,
{"default": model_list[0]}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
"api_url":(list(llm_apis_dict.keys()),
{"default": list(llm_apis_dict.keys())[0]}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
"optional":{
"api_key":("STRING", {"forceInput": True,}),
"custom_model_name":("STRING", {"forceInput": True,}), #适合自定义model
"custom_api_url":("STRING", {"forceInput": True,}), #适合自定义model
},
}
RETURN_TYPES = ("STRING","STRING","STRING",)
@@ -255,12 +352,29 @@ class ChatGPTNode:
def generate_contextual_text(self,
api_key,
api_url,
# api_key,
prompt,
system_content,
model,
seed,context_size,unique_id = None, extra_pnginfo=None):
model,
seed,
context_size,
api_url,
api_key=None,
custom_model_name=None,
custom_api_url=None,
):
if custom_model_name!=None:
model=custom_model_name
api_url=llm_apis_dict[api_url] if api_url in llm_apis_dict else ""
if custom_api_url!=None:
api_url=custom_api_url
if api_key==None:
api_key="lm_studio"
# print(api_key!='',api_url,prompt,system_content,model,seed)
# 可以选择保留会话历史以维持上下文记忆
# 或者在此处清除会话历史 self.session_history.clear()
@@ -273,7 +387,7 @@ class ChatGPTNode:
self.system_content=system_content
# self.session_history=[]
# self.session_history.append({"role": "system", "content": system_content})
print("api_key,api_url",api_key,api_url)
#
if is_azure_url(api_url):
client=azure_client(api_key,api_url)
@@ -282,12 +396,12 @@ class ChatGPTNode:
if model == "glm-4" :
client = ZhipuAI_client(api_key) # 使用 Zhipuai 的接口
print('using Zhipuai interface')
elif model in llama_modes_list:
#
client=llama_cpp_client(model)
# elif model in llama_modes_list:
# #
# client=llama_cpp_client(model)
else :
client = openai_client(api_key,api_url) # 使用 ChatGPT 的接口
print('using ChatGPT interface')
# print('using ChatGPT interface',api_key,api_url)
# 把用户的提示添加到会话历史中
# 调用API时传递整个会话历史
@@ -303,6 +417,7 @@ class ChatGPTNode:
session_history=crop_list_tail(self.session_history,context_size)
messages=[{"role": "system", "content": self.system_content}]+session_history+[{"role": "user", "content": prompt}]
response_content = chat(client,model,messages)
self.session_history=self.session_history+[{"role": "user", "content": prompt}]+[{'role':'assistant',"content":response_content}]
@@ -323,6 +438,93 @@ class ChatGPTNode:
return (response_content,json.dumps(messages, indent=4),json.dumps(self.session_history, indent=4),)
class SiliconflowFreeNode:
def __init__(self):
# self.__client = OpenAI()
self.session_history = [] # 用于存储会话历史的列表
# self.seed=0
self.system_content="You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible."
@classmethod
def INPUT_TYPES(cls):
model_list= [
"Qwen/Qwen2-7B-Instruct",
"THUDM/glm-4-9b-chat",
"01-ai/Yi-1.5-9B-Chat-16K",
"meta-llama/Meta-Llama-3.1-8B-Instruct"
]
return {
"required": {
"api_key":("STRING", {"forceInput": True,}),
"prompt": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"system_content": ("STRING",
{
"default": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"multiline": True,"dynamicPrompts": False
}),
"model": ( model_list,
{"default": model_list[0]}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
},
"optional":{
"custom_model_name":("STRING", {"forceInput": True,}), #适合自定义model
},
}
RETURN_TYPES = ("STRING","STRING","STRING",)
RETURN_NAMES = ("text","messages","session_history",)
FUNCTION = "generate_contextual_text"
CATEGORY = "♾️Mixlab/GPT"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,False,)
def generate_contextual_text(self,
api_key,
prompt,
system_content,
model,
seed,context_size,custom_model_name=None):
if custom_model_name!=None:
model=custom_model_name
api_url="https://api.siliconflow.cn/v1"
# 把系统信息和初始信息添加到会话历史中
if system_content:
self.system_content=system_content
# self.session_history=[]
# self.session_history.append({"role": "system", "content": system_content})
#
client = openai_client(api_key,api_url) # 使用 ChatGPT 的接口
# print('using ChatGPT interface',api_key,api_url)
# 把用户的提示添加到会话历史中
# 调用API时传递整个会话历史
def crop_list_tail(lst, size):
if size >= len(lst):
return lst
elif size==0:
return []
else:
return lst[-size:]
session_history=crop_list_tail(self.session_history,context_size)
messages=[{"role": "system", "content": self.system_content}]+session_history+[{"role": "user", "content": prompt}]
response_content = chat(client,model,messages)
self.session_history=self.session_history+[{"role": "user", "content": prompt}]+[{'role':'assistant',"content":response_content}]
return (response_content,json.dumps(messages, indent=4),json.dumps(self.session_history, indent=4),)
class ShowTextForGPT:
@classmethod
@@ -484,3 +686,41 @@ class TextSplitByDelimiter:
arr= arr[start_index:start_index + max_count * (skip_every+1):(skip_every+1)]
return (arr,)
class JsonRepair:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"json_string":("STRING", {"forceInput": True,}),
"key":("STRING", {"multiline": False,"dynamicPrompts": False,"default": ""}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("json_string","value",)
FUNCTION = "run"
# OUTPUT_NODE = True
OUTPUT_IS_LIST = (False,False,)
CATEGORY = "♾️Mixlab/GPT"
def run(self, json_string,key=""):
json_string=extract_json_strings(json_string)
# print(json_string)
good_json_string = repair_json(json_string)
# 将 JSON 字符串解析为 Python 对象
data = json.loads(good_json_string)
v=""
if key!="" and (key in data):
v=data[key]
# 将 Python 对象转换回 JSON 字符串,确保中文字符不被转义
json_str_with_chinese = json.dumps(data, ensure_ascii=False)
return (json_str_with_chinese,v,)
+9 -3
View File
@@ -70,14 +70,20 @@ def load_caption_model(model_path,config,t='blip-base'):
return (caption_model,caption_processor)
def get_clip_interrogator_path():
try:
return folder_paths.get_folder_paths('clip_interrogator')[0]
except:
return os.path.join(folder_paths.models_dir, "clip_interrogator")
caption_model_path=os.path.join(folder_paths.models_dir, "clip_interrogator/Salesforce/blip-image-captioning-base")
cache_path=get_clip_interrogator_path()
caption_model_path=os.path.join(cache_path, "Salesforce","blip-image-captioning-base")
if not os.path.exists(caption_model_path):
print(f"## clip_interrogator_model not found: {caption_model_path}, pls download from https://huggingface.co/Salesforce/blip-image-captioning-base")
caption_model_path='Salesforce/blip-image-captioning-base'
cache_path=os.path.join(folder_paths.models_dir, "clip_interrogator")
# Tensor to PIL
def tensor2pil(image):
+508 -273
View File
@@ -1,12 +1,14 @@
import numpy as np
import requests
import torch
import torchvision.transforms.v2 as T
# from PIL import Image, ImageDraw
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
import base64,os,random
from io import BytesIO
import folder_paths
import node_helpers
import json,io
import comfy.utils
from comfy.cli_args import args
@@ -14,8 +16,8 @@ import cv2
import string
import math,glob
from .Watcher import FolderWatcher
import hashlib
from itertools import product
# 将PIL图片转换为OpenCV格式
@@ -28,142 +30,105 @@ def opencv_to_pil(image):
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
return pil_image
# 列出目录下面的所有文件
def get_files_with_extension(directory, extensions):
file_list = []
# 确保extensions参数是一个list,即使只有一个元素
if not isinstance(extensions, (tuple, list)):
extensions = [extensions]
for root, dirs, files in os.walk(directory):
# print(f"Files at {root}: {files}") # 确认files是一个字符串列表
for file in files:
# 检查文件是否以任何一个提供的扩展名结尾
if any(file.endswith(ext) for ext in extensions):
# 直接将文件名添加到列表中
file_list.append(file)
return file_list
def composite_images(foreground, background, mask, is_multiply_blend=False, position="overall", scale=0.25):
width, height = foreground.size
bg_image = background
bwidth, bheight = bg_image.size
def composite_images(foreground, background, mask,is_multiply_blend=False,position="overall"):
width,height=foreground.size
bg_image=background
scale=max(scale,1/bwidth)
scale=max(scale,1/bheight)
bwidth,bheight=bg_image.size
def determine_scale_option(width, height):
return 'height' if height > width else 'width'
# 按z-index排序
if position=="overall":
if position == "overall":
layer = {
"x":0,
"y":0,
"width":bwidth,
"height":bheight,
"z_index":88,
"scale_option":'overall',
"image":foreground,
"mask":mask
"x": 0,
"y": 0,
"width": bwidth,
"height": bheight,
"z_index": 88,
"scale_option": 'overall',
"image": foreground,
"mask": mask
}
else:
scale_option = determine_scale_option(width, height)
if scale_option == 'height':
scale = int(bheight * scale) / height
else:
scale = int(bwidth * scale) / width
elif position=='center_bottom':
scale = int(bwidth*0.25) / width
new_width = int(width * scale)
new_height = int(height * scale)
layer = {
"x":int(bwidth*0.75*0.5),
"y":bheight-new_height-24,
"width":int(bwidth*0.25),
"height":int(bheight*0.25),
"z_index":88,
"scale_option":'width',
"image":foreground,
"mask":mask
}
elif position=='right_bottom':
scale = int(bwidth*0.25) / width
new_height = int(height * scale)
if position == 'center_bottom':
x_position = int((bwidth - new_width) * 0.5)
y_position = bheight - new_height - 24
elif position == 'right_bottom':
x_position = bwidth - new_width - 24
y_position = bheight - new_height - 24
elif position == 'center_top':
x_position = int((bwidth - new_width) * 0.5)
y_position = 24
elif position == 'right_top':
x_position = bwidth - new_width - 24
y_position = 24
elif position == 'left_top':
x_position = 24
y_position = 24
elif position == 'left_bottom':
x_position = 24
y_position = bheight - new_height - 24
elif position == 'center_center':
x_position = int((bwidth - new_width) * 0.5)
y_position = int((bheight - new_height) * 0.5)
layer = {
"x":bwidth-int(bwidth*0.25)-24,
"y":bheight-new_height-24,
"width":int(bwidth*0.25),
"height":int(bheight*0.25),
"z_index":88,
"scale_option":'width',
"image":foreground,
"mask":mask
"x": x_position,
"y": y_position,
"width": new_width,
"height": new_height,
"z_index": 88,
"scale_option": scale_option,
"image": foreground,
"mask": mask
}
layer_image = layer['image']
layer_mask = layer['mask']
elif position=='center_top':
scale = int(bwidth*0.25) / width
new_height = int(height * scale)
bg_image = merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option'],
is_multiply_blend)
layer = {
"x":int( bwidth*0.75*0.5),
"y":24,
"width":int(bwidth*0.25),
"height":int(bheight*0.25),
"z_index":88,
"scale_option":'width',
"image":foreground,
"mask":mask
}
bg_image = bg_image.convert('RGB')
elif position=='right_top':
scale = int(bwidth*0.25) / width
new_height = int(height * scale)
layer = {
"x":bwidth-int(bwidth*0.25)-24,
"y":24,
"width":int(bwidth*0.25),
"height":int(bheight*0.25),
"z_index":88,
"scale_option":'width',
"image":foreground,
"mask":mask
}
elif position=='left_top':
scale = int(bwidth*0.25) / width
new_height = int(height * scale)
layer = {
"x":24,
"y":24,
"width":int(bwidth*0.25),
"height":int(bheight*0.25),
"z_index":88,
"scale_option":'width',
"image":foreground,
"mask":mask
}
elif position=='left_bottom':
scale = int(bwidth*0.25) / width
new_height = int(height * scale)
layer = {
"x":24,
"y":bheight-new_height-24,
"width":int(bwidth*0.25),
"height":int(bheight*0.25),
"z_index":88,
"scale_option":'width',
"image":foreground,
"mask":mask
}
# width, height = bg_image.size
layer_image=layer['image']
layer_mask=layer['mask']
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option'],
is_multiply_blend )
bg_image=bg_image.convert('RGB')
return bg_image
def count_files_in_directory(directory):
file_count = 0
for _, _, files in os.walk(directory):
@@ -200,7 +165,8 @@ class AnyType(str):
any_type = AnyType("*")
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),"..","assets","fonts"))
MAX_RESOLUTION=8192
@@ -526,6 +492,53 @@ def load_image(fp,white_bg=False):
return images
# 读取图片数据,转成tensor
def load_image_to_tensor( image):
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
def load_image_and_mask_from_url(url, timeout=10):
# Load the image from the URL
response = requests.get(url, timeout=timeout)
@@ -802,85 +815,78 @@ def multiply_blend(image1, image2):
# cv2.imwrite('result.jpg', result)
# 使用gpt4o优化代码
# 为了消除图像合并时出现的灰色描边,可以使用以下方法:
# 调整透明度:确保透明像素不会引入不需要的颜色。
# 预处理图像:在缩放图像之前,可以先将图像的边缘进行预处理,例如扩展边缘颜色,减少抗锯齿带来的过渡效果。
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option,is_multiply_blend=False):
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option, is_multiply_blend=False):
# 打开底图
bg_image = bg_image.convert("RGBA")
# 打开图层
layer_image = layer_image.convert("RGBA")
# layer_image = layer_image.resize((width, height))
# 根据缩放选项调整图像大小
if scale_option == "height":
# 按照高度比例缩放
original_width, original_height = layer_image.size
scale = height / original_height
new_width = int(original_width * scale)
layer_image = layer_image.resize((new_width, height))
layer_image = layer_image.resize((new_width, height), Image.NEAREST)
elif scale_option == "width":
# 按照宽度比例缩放
original_width, original_height = layer_image.size
scale = width / original_width
new_height = int(original_height * scale)
layer_image = layer_image.resize((width, new_height))
layer_image = layer_image.resize((width, new_height), Image.NEAREST)
elif scale_option == "overall":
# 整体缩放
layer_image = layer_image.resize((width, height))
layer_image = layer_image.resize((width, height), Image.NEAREST)
elif scale_option == "longest":
original_width, original_height = layer_image.size
if original_width > original_height:
new_width=width
new_width = width
scale = width / original_width
new_height = int(original_height * scale)
x=0
y=int((height-new_height)*0.5)
x = 0
y = int((height - new_height) * 0.5)
else:
new_height=height
new_height = height
scale = height / original_height
new_width = int(original_height * scale)
x=int((width-new_width)*0.5)
y=0
# elif side == "shortest":
# if width < height:
#
# else:
#
x = int((width - new_width) * 0.5)
y = 0
# 调整mask的大小
nw, nh = layer_image.size
mask = mask.resize((nw, nh))
mask = mask.resize((nw, nh), Image.NEAREST)
# # 分离出a通道
# r, g, b, alpha = layer_image.split()
# alpha = ImageOps.invert(alpha)
# # 创建一个新的RGB图像
# new_rgb_image = Image.new("RGB", layer_image.size)
# # 将透明通道粘贴到新的RGB图像上
# new_rgb_image.paste(layer_image, (0, 0), mask=alpha)
# new_rgb_image.paste(layer_image, (x, y), mask=mask)
# mask=new_rgb_image.convert('L')
# mask = ImageOps.invert(mask)
# 预处理图像边缘以减少灰色描边
layer_image = layer_image.filter(ImageFilter.SMOOTH)
if is_multiply_blend:
bg_image_white=Image.new("RGB", bg_image.size,(255, 255, 255))
bg_image_white = Image.new("RGB", bg_image.size, (255, 255, 255))
bg_image_white.paste(layer_image, (x, y), mask=mask)
bg_image=multiply_blend(bg_image_white,bg_image)
bg_image=bg_image.convert("RGBA")
bg_image = multiply_blend(bg_image_white, bg_image)
bg_image = bg_image.convert("RGBA")
else:
transparent_img = Image.new("RGBA",layer_image.size, (255, 255, 255, 0))
transparent_img.paste(layer_image,(0, 0), mask)
# transparent_img.save('test.png')
bg_image.paste(transparent_img, (x, y), transparent_img)
transparent_img = Image.new("RGBA", layer_image.size, (255, 255, 255, 0))
# 调整透明度处理
for i in range(transparent_img.size[0]):
for j in range(transparent_img.size[1]):
r, g, b, a = transparent_img.getpixel((i, j))
if a > 0:
transparent_img.putpixel((i, j), (r, g, b, 255))
transparent_img.paste(layer_image, (0, 0), mask)
bg_image.paste(transparent_img, (x, y), transparent_img)
# 输出合成后的图片
return bg_image
#MixCopilot
def resize_2(img):
# 检查图像的高度是否是2的倍数,如果不是,则调整高度
@@ -954,53 +960,13 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
return layer_image
# def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
# # Load Chinese font
# font = ImageFont.truetype(font_path, font_size)
# # Calculate image size based on the number of characters and orientation
# if vertical:
# width = font_size + 100
# height = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
# else:
# width = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
# height = font_size + 100
# # Create a blank image
# image = Image.new('RGBA', (width, height), (255, 255, 255,0))
# draw = ImageDraw.Draw(image)
# # Draw text
# if vertical:
# for i, char in enumerate(text_list):
# char_position = (50, 50 + i * font_size)
# draw.text(char_position, char, font=font, fill=text_color)
# else:
# for i, char in enumerate(text_list):
# char_position = (50 + i * (font_size + spacing), 50)
# draw.text(char_position, char, font=font, fill=text_color)
# # Save the image
# # image.save(output_image_path)
# # 分离alpha通道
# alpha_channel = image.split()[3]
# # 创建一个只有alpha通道的新图像
# alpha_image = Image.new('L', image.size)
# alpha_image.putdata(alpha_channel.getdata())
# image=image.convert('RGB')
# return (image,alpha_image)
def generate_text_image(text, font_path, font_size, text_color, vertical=True, stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0):
def generate_text_image(text, font_path, font_size, text_color, vertical=True, stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0, line_spacing=0,padding=4):
# Split text into lines based on line breaks
lines = text.split("\n")
# Load font
font = ImageFont.truetype(font_path, font_size)
# 1. Determine layout direction
if vertical:
layout = "vertical"
@@ -1009,49 +975,54 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
# 2. Calculate absolute coordinates for each character
char_coordinates = []
if layout == "vertical":
x = 0
y = 0
for i in range(len(lines)):
line = lines[i]
for char in line:
char_coordinates.append((x, y))
y += font_size + spacing
x += font_size + spacing
y = 0
else:
x = 0
y = 0
for line in lines:
for char in line:
char_coordinates.append((x, y))
x += font_size + spacing
y += font_size + spacing
x = 0
x, y = padding, padding
max_width, max_height = 0, 0
# 3. Calculate image width and height
if layout == "vertical":
width = (len(lines) * (font_size + spacing)) - spacing
height = ((len(max(lines, key=len)) + 1) * (font_size + spacing)) + spacing
for line in lines:
max_char_width = max(font.getsize(char)[0] for char in line)
for char in line:
char_width, char_height = font.getsize(char)
char_coordinates.append((x, y))
y += char_height + spacing
max_height = max(max_height, y + padding)
x += max_char_width + line_spacing
y = padding
max_width = x
total_line_width = sum(font.getsize(line)[1] for line in lines)
total_spacing = line_spacing * (len(lines) - 1)
# 确保左边和右边的padding都被计入max_width
max_width = total_line_width + total_spacing + padding * 2
else:
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
height = ((len(lines) - 1) * (font_size + spacing)) + font_size
for line in lines:
line_width, line_height = font.getsize(line)
for char in line:
char_width, char_height = font.getsize(char)
char_coordinates.append((x, y))
x += char_width + spacing
max_width = max(max_width, x + padding)
y += line_height + line_spacing
x = padding
# max_height = y
total_line_heights = sum(font.getsize(line)[1] for line in lines)
total_spacing = line_spacing * (len(lines) - 1)
# 确保顶部和底部的padding都被计入max_height
max_height = total_line_heights + total_spacing + padding * 2
# 3. Create image with calculated width and height
image = Image.new('RGBA', (max_width, max_height), (255, 255, 255, 0))
draw = ImageDraw.Draw(image)
# 4. Draw each character on the image
image = Image.new('RGBA', (width, height), (255, 255, 255, 0))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(font_path, font_size)
index = 0
for i, line in enumerate(lines):
for j, char in enumerate(line):
for line in lines:
for char in line:
x, y = char_coordinates[index]
if stroke:
draw.text((x-stroke_width, y), char, font=font, fill=stroke_color)
draw.text((x+stroke_width, y), char, font=font, fill=stroke_color)
draw.text((x, y-stroke_width), char, font=font, fill=stroke_color)
draw.text((x, y+stroke_width), char, font=font, fill=stroke_color)
draw.text((x-stroke_width, y), char, font=font, fill=text_color)
draw.text((x+stroke_width, y), char, font=font, fill=text_color)
draw.text((x, y-stroke_width), char, font=font, fill=text_color)
draw.text((x, y+stroke_width), char, font=font, fill=text_color)
draw.text((x, y), char, font=font, fill=text_color)
index += 1
@@ -1376,6 +1347,9 @@ class LoadImages_:
image=pil2tensor(image)
ims.append(image)
if len(ims)==0:
image1 = Image.new('RGB', (512, 512), color='black')
return (pil2tensor(image1),)
image1 = ims[0]
for image2 in ims[1:]:
if image1.shape[1:] != image2.shape[1:]:
@@ -1578,7 +1552,7 @@ class ImageCropByAlpha:
# get_files_with_extension(FONT_PATH,'.ttf')
class TextImage:
@classmethod
@@ -1586,18 +1560,32 @@ class TextImage:
return {"required": {
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲","dynamicPrompts": False}),
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH,"dynamicPrompts": False}),
"font": (get_files_with_extension(FONT_PATH,['.ttf','.otf']),),#后缀为 ttf
"font_size": ("INT",{
"default":100,
"min": 100, #Minimum value
"max": 1000, #Maximum value
"min": 1, #Minimum value
"max": 10000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"spacing": ("INT",{
"default":12,
"min": -200, #Minimum value
"max": 200, #Maximum value
"min": -2000000000, #Minimum value
"max": 2000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"line_spacing": ("INT",{
"default":12,
"min": -2000000000, #Minimum value
"max": 2000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"padding": ("INT",{
"default":8,
"min": 0, #Minimum value
"max": 2000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
@@ -1608,7 +1596,7 @@ class TextImage:
}
RETURN_TYPES = ("IMAGE","MASK",)
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
RETURN_NAMES = ("image","mask",)
FUNCTION = "run"
@@ -1617,11 +1605,14 @@ class TextImage:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,text,font_path,font_size,spacing,text_color,vertical,stroke):
def run(self,text,font,font_size,spacing,line_spacing,padding,text_color,vertical,stroke):
# text_list=list(text)
font_path=os.path.join(FONT_PATH,font)
if text=="":
text=" "
# stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,stroke,(0, 0, 0),1,spacing)
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,stroke,(0, 0, 0),1,spacing,line_spacing,padding)
img=pil2tensor(img)
mask=pil2tensor(mask)
@@ -1655,7 +1646,7 @@ class LoadImagesFromURL:
def run(self,url,seed=0):
global urls_image
print(urls_image)
# print(urls_image)
def filter_http_urls(urls):
filtered_urls = []
for url in urls.split('\n'):
@@ -1744,28 +1735,51 @@ class Image3D:
def run(self,upload,material=None):
# print('material',material)
# print(upload )
image = base64_to_image(upload['image'])
mat=None
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
# 截取的系列角度截图
images=upload['images'] if "images" in upload else []
mask = image.split()[3]
image=image.convert('RGB')
ims=[]
for im in images:
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
mask=mask.convert('L')
mask=None
bg_image=None
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mat=None
# 如果没有系列截图
if len(ims)==0:
# 这个是3d模型当前截图
image = base64_to_image(upload['image'])
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
mask = image.split()[3]
image=image.convert('RGB')
mask=mask.convert('L')
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mask=pil2tensor(mask)
image=pil2tensor(image)
mask=pil2tensor(mask)
image=pil2tensor(image)
else:
image = torch.cat(ims, dim=0)
m=[]
if not material is None:
@@ -1854,10 +1868,16 @@ class CompositeImages:
"mask":("MASK",),
"background": ("IMAGE",),
},
"optional":{
"optional":{
"is_multiply_blend": ("BOOLEAN", {"default": False}),
"position": (['overall',"center_bottom","center_top","right_bottom","left_bottom","right_top","left_top"],),
"position": (['overall',"center_center","left_bottom","center_bottom","right_bottom","left_top","center_top","right_top"],),
"scale": ("FLOAT",{
"default":0.35,
"min": 0.01, #Minimum value
"max": 1, #Maximum value
"step": 0.01, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
}
}
@@ -1870,15 +1890,30 @@ class CompositeImages:
# OUTPUT_IS_LIST = (True,)
def run(self, foreground,mask,background,is_multiply_blend,position):
foreground= tensor2pil(foreground)
mask= tensor2pil(mask)
background= tensor2pil(background)
res=composite_images(foreground,background,mask,is_multiply_blend,position)
# def run(self, foreground,mask,background,is_multiply_blend,position,scale):
# foreground= tensor2pil(foreground)
# mask= tensor2pil(mask)
# background= tensor2pil(background)
# res=composite_images(foreground,background,mask,is_multiply_blend,position,scale)
return (pil2tensor(res),)
# return (pil2tensor(res),)
def run(self, foreground,mask,background, is_multiply_blend, position, scale):
results = []
f1=[]
for fg, mask in zip(foreground, mask ):
f1.append([fg,mask])
for f, bg in product(f1, background):
[fg,mask]=f
fg_pil = tensor2pil(fg)
mask_pil = tensor2pil(mask)
bg_pil = tensor2pil(bg)
res = composite_images(fg_pil, bg_pil, mask_pil, is_multiply_blend, position, scale)
results.append(pil2tensor(res))
output_image = torch.cat(results, dim=0)
return (output_image,)
class EmptyLayer:
@@ -2983,6 +3018,65 @@ class SaveImageAndMetadata:
return { "ui": { "images": results } }
class ComparingTwoFrames:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = "ComparingTwoFrames"
self.compress_level = 4
@classmethod
def INPUT_TYPES(s):
return {"required":
{"before_image": ("IMAGE", ),
"after_image": ("IMAGE", )
},
}
RETURN_TYPES = ()
FUNCTION = "comparingImages"
OUTPUT_NODE = True
CATEGORY = "♾️Mixlab/Output"
def comparingImages(self, before_image,after_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, after_image[0].shape[1], after_image[0].shape[0])
bresults = list()
for bimage in before_image:
i = 255. * bimage.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
file = f"{filename}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file), pnginfo=None, compress_level=self.compress_level)
bresults.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
results = list()
for aimage in after_image:
i = 255. * aimage.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
file = f"{filename}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file), pnginfo=None, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
return { "ui": { "after_images": results,"before_images":bresults } }
class ImageColorTransfer:
@classmethod
def INPUT_TYPES(s):
@@ -3148,3 +3242,144 @@ class SaveImageToLocal:
counter += 1
return ()
class ImageBatchToList_:
@classmethod
def INPUT_TYPES(s):
return {"required": {"image_batch": ("IMAGE",), }}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image_list",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Image"
def run(self, image_batch):
images = [image_batch[i:i + 1, ...] for i in range(image_batch.shape[0])]
return (images, )
class ImageListToBatch_:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
INPUT_IS_LIST = True
CATEGORY = "♾️Mixlab/Image"
def run(self, images):
shape = images[0].shape[1:3]
out = []
for i in range(len(images)):
img = images[i].permute([0,3,1,2])
if images[i].shape[1:3] != shape:
transforms = T.Compose([
T.CenterCrop(min(img.shape[2], img.shape[3])),
T.Resize((shape[0], shape[1]), interpolation=T.InterpolationMode.BICUBIC),
])
img = transforms(img)
out.append(img.permute([0,2,3,1]))
out = torch.cat(out, dim=0)
return (out,)
# https://github.com/gokayfem/ComfyUI-Depth-Visualization?tab=readme-ov-file
class DepthViewer_:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"depth_map": ("IMAGE",),
},
"optional":{
"frames":("IMAGEBASE64",),
},
}
def __init__(self):
self.saved_reference = []
self.saved_depth = []
self.full_output_folder,self.filename,self.counter, self.subfolder, self.filename_prefix = folder_paths.get_save_image_path(
"imagesave",
folder_paths.get_output_directory())
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("frames",)
OUTPUT_NODE = True
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/3D"
def run(self, image, depth_map,frames=None):
self.saved_reference.clear()
self.saved_depth.clear()
image = image[0].detach().cpu().numpy()
depth = depth_map[0].detach().cpu().numpy()
image = Image.fromarray(np.clip(255. * image, 0, 255).astype(np.uint8)).convert('RGB')
depth = Image.fromarray(np.clip(255. * depth, 0, 255).astype(np.uint8))
return self.display([image], [depth],frames)
def display(self, reference_image, depth_map,frames):
for (batch_number, (single_image, single_depth)) in enumerate(zip(reference_image, depth_map)):
filename_with_batch_num = self.filename.replace("%batch_num%", str(batch_number))
image_file = f"{filename_with_batch_num}_{self.counter:05}_reference.png"
single_image.save(os.path.join(self.full_output_folder, image_file))
depth_file = f"{filename_with_batch_num}_{self.counter:05}_depth.png"
single_depth.save(os.path.join(self.full_output_folder, depth_file))
self.saved_reference.append({
"filename": image_file,
"subfolder": self.subfolder,
"type": "output"
})
self.saved_depth.append({
"filename": depth_file,
"subfolder": self.subfolder,
"type": "output"
})
self.counter += 1
ims=[]
image1 = Image.new('RGB', (512, 512), color='black')
image1=pil2tensor(image1)
if frames!=None:
for im in frames['images']:
# print(im)
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
if len(ims)>0:
image1 = ims[0]
for image2 in ims[1:]:
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
return {"ui": {"reference_image": self.saved_reference, "depth_map": self.saved_depth}, "result": (image1,)}
+7 -4
View File
@@ -42,8 +42,13 @@ else:
_available=True
llma_model_path=os.path.join(folder_paths.models_dir, "lama/big-lama.pt")
def get_lama_path():
try:
return folder_paths.get_folder_paths('lama')[0]
except:
return os.path.join(folder_paths.models_dir, "lama")
llma_model_path=os.path.join(get_lama_path(), "big-lama.pt")
if not os.path.exists(llma_model_path):
os.environ['LAMA_MODEL']=''
print(f"## lama torchscript model not found: {llma_model_path},pls download from https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt")
@@ -80,8 +85,6 @@ class LaMaInpainting:
"image": ("IMAGE",),
"mask": ("MASK",),
},
}
RETURN_TYPES = ("IMAGE",)
+127
View File
@@ -0,0 +1,127 @@
# Referenced some code:https://github.com/IuvenisSapiens/ComfyUI_MiniCPM-V-2_6-int4
import os
import torch
import folder_paths
from transformers import AutoTokenizer, AutoModel
from torchvision.transforms.v2 import ToPILImage
from decord import VideoReader, cpu # pip install decord
from PIL import Image
def get_model_path(n=""):
try:
return folder_paths.get_folder_paths(n)[0]
except:
return os.path.join(folder_paths.models_dir, n)
class MiniCPM_VQA_Simple:
def __init__(self):
self.model_checkpoint = None
self.tokenizer = None
self.model = None
self.device = (
torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
)
self.bf16_support = (
torch.cuda.is_available()
and torch.cuda.get_device_capability(self.device)[0] >= 8
)
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"text": ("STRING", {"default": "", "multiline": True}),
"seed": ("INT", {"default": -1}), # add seed parameter, default is -1
"temperature": (
"FLOAT",
{
"default": 0.7,
},
),
"keep_model_loaded": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "inference"
CATEGORY = "♾️Mixlab/Image"
def inference(
self,
images,
text,
seed, # add seed parameter, default is -1
temperature,
keep_model_loaded,
):
if seed != -1:
torch.manual_seed(seed)
model_id = "openbmb/MiniCPM-V-2_6-int4"
self.model_checkpoint = os.path.join( get_model_path("prompt_generator"), os.path.basename(model_id))
if not os.path.exists(self.model_checkpoint):
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=model_id,
local_dir=self.model_checkpoint,
local_dir_use_symlinks=False,
endpoint='https://hf-mirror.com'
)
if self.tokenizer is None:
self.tokenizer = AutoTokenizer.from_pretrained(
self.model_checkpoint,
trust_remote_code=True,
low_cpu_mem_usage=True,
)
if self.model is None:
self.model = AutoModel.from_pretrained(
self.model_checkpoint,
trust_remote_code=True,
low_cpu_mem_usage=True,
attn_implementation="sdpa",
torch_dtype=torch.bfloat16 if self.bf16_support else torch.float16,
)
with torch.no_grad():
images = images.permute([0, 3, 1, 2])
images = [ToPILImage()(img).convert("RGB") for img in images]
msgs = [{"role": "user", "content": images + [text]}]
params = {"use_image_id": False, }
# offload model to CPU
# self.model = self.model.to(torch.device("cpu"))
# self.model.eval()
result = self.model.chat(
image=None,
msgs=msgs,
tokenizer=self.tokenizer,
sampling=True,
# top_k=top_k,
# top_p=top_p,
temperature=temperature,
# repetition_penalty=repetition_penalty,
# max_new_tokens=max_new_tokens,
**params,
)
# offload model to GPU
# self.model = self.model.to(torch.device("cpu"))
# self.model.eval()
if not keep_model_loaded:
del self.tokenizer # release tokenizer memory
del self.model # release model memory
self.tokenizer = None # set tokenizer to None
self.model = None # set model to None
torch.cuda.empty_cache() # release GPU memory
torch.cuda.ipc_collect()
return (result,)
+104
View File
@@ -0,0 +1,104 @@
import torch
import numpy as np
from PIL import Image,ImageSequence,ImageOps
import base64
import io
import comfy.utils
import folder_paths
import node_helpers
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# Convert PIL to Tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def load_image_to_tensor( image):
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
class P5Input:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"frames":("IMAGEBASE64",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("frames",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Input"
OUTPUT_NODE = True
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self, frames):
ims=[]
for im in frames['images']:
# print(im)
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
if len(ims)==0:
image1 = Image.new('RGB', (512, 512), color='black')
return (pil2tensor(image1),)
image1 = ims[0]
for image2 in ims[1:]:
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
# 用于节点提示:p5节点提示有多少帧
return {"ui": {"_info": [len(frames['images'])]}, "result": (image1,)}
+7 -1
View File
@@ -18,7 +18,13 @@ import json
# req = request.Request("http://127.0.0.1:8188/prompt", data=data)
# request.urlopen(req)
embeddings_path=os.path.join(folder_paths.models_dir, "embeddings")
def get_model_path(n=""):
try:
return folder_paths.get_folder_paths(n)[0]
except:
return os.path.join(folder_paths.models_dir, n)
embeddings_path=get_model_path("embeddings")
def get_files_with_extension(directory, extension):
+36 -22
View File
@@ -467,15 +467,37 @@ class BriaRMBG(nn.Module):
def get_U2NET_model_path():
try:
return folder_paths.get_folder_paths('rembg')[0]
except:
return os.path.join(folder_paths.models_dir, "rembg")
U2NET_HOME=os.path.join(folder_paths.models_dir, "rembg")
U2NET_HOME=get_U2NET_model_path()
os.environ["U2NET_HOME"] = U2NET_HOME
global _available
_available=False
def get_rembg_models(path):
"""从目录中获取文件并提取文件名
Args:
path: 目录路径
Returns:
文件名列表
"""
filenames = []
for root, _, files in os.walk(path):
for filename in files:
# 过滤隐藏文件
if not filename.startswith('.'):
name, ext = os.path.splitext(os.path.basename(filename))
filenames.append(name)
return filenames
def is_installed(package):
try:
spec = importlib.util.find_spec(package)
@@ -509,8 +531,8 @@ except:
_available=False
def briarmbg_run(images=[]):
mroot=os.path.join(folder_paths.models_dir, "rembg")
def run_briarmbg(images=[]):
mroot=U2NET_HOME
m=os.path.join(mroot,'briarmbg.pth')
if os.path.exists(m)==False:
# 下载
@@ -573,14 +595,15 @@ def briarmbg_run(images=[]):
return (masks,rgba_images,rgb_images)
def run_bg(model_name= "unet",images=[]):
def run_rembg(model_name= "unet",images=[],callback=None):
# model_name = "unet" # "isnet-general-use"
# print('#run_rembg',model_name)
rembg_session = new_session(model_name)
masks=[]
rgba_images=[]
rgb_images=[]
# 进度条
pbar = comfy.utils.ProgressBar(len(images) )
pbar=callback
for img in images:
# use the post_process_mask argument to post process the mask to get better results.
mask = remove(img, session=rembg_session,only_mask=True,post_process_mask=True)
@@ -620,8 +643,9 @@ def run_bg(model_name= "unet",images=[]):
rgb_image = Image.new("RGB", image_rgba.size, (0, 0, 0))
rgb_image.paste(image_rgba, mask=image_rgba.split()[3])
rgb_images.append(rgb_image)
pbar.update(1)
if pbar:
pbar.update(1)
return (masks,rgba_images,rgb_images)
@@ -643,17 +667,7 @@ class RembgNode_:
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"model_name": ([
"briarmbg",
"u2net",
"u2netp",
"u2net_human_seg",
"u2net_cloth_seg",
"silueta",
"isnet-general-use",
"isnet-anime",
],),
"model_name": (get_rembg_models(U2NET_HOME),),
},
}
@@ -681,9 +695,9 @@ class RembgNode_:
images.append(im)
if model_name=='briarmbg':
masks,rgba_images,rgb_images=briarmbg_run(images)
masks,rgba_images,rgb_images=run_briarmbg(images)
else:
masks,rgba_images,rgb_images=run_bg(model_name,images)
masks,rgba_images,rgb_images=run_rembg(model_name,images, comfy.utils.ProgressBar(len(images) ))
masks=[pil2tensor(m) for m in masks]
+6 -6
View File
@@ -90,7 +90,7 @@ class ScreenShareNode:
} }
RETURN_TYPES = ('IMAGE','STRING','FLOAT',"INT")
RETURN_NAMES = ("IMAGE","PROMPT","FLOAT","INT")
RETURN_NAMES = ("current frame (image)","prompt","denoise (float)","seed (int)")
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Screen"
@@ -109,7 +109,7 @@ class FloatingVideo:
@classmethod
def INPUT_TYPES(s):
return { "required":{
"images": ("IMAGE",)
"image": ("IMAGE",)
}, }
# RETURN_TYPES = ('IMAGE','MASK')
@@ -124,16 +124,16 @@ class FloatingVideo:
# OUTPUT_IS_LIST = (False,False,)
# 运行的函数
def run(self,images):
def run(self,image):
results = list()
for image in images:
image=tensor2pil(image)
for im in image:
im=tensor2pil(im)
# image_base64 = base64.b64encode(image.tobytes())
buffered = BytesIO()
image.save(buffered, format="JPEG")
im.save(buffered, format="JPEG")
image_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
results.append(image_base64)
+19 -11
View File
@@ -18,19 +18,25 @@ from lark import Lark, Transformer, v_args
global _available
_available=True
def get_text_generator_path():
try:
return folder_paths.get_folder_paths('prompt_generator')[0]
except:
return os.path.join(folder_paths.models_dir, "prompt_generator")
text_generator_model_path=os.path.join(folder_paths.models_dir, "prompt_generator/text2image-prompt-generator")
prompt_generator=get_text_generator_path()
text_generator_model_path=os.path.join(prompt_generator, "text2image-prompt-generator")
if not os.path.exists(text_generator_model_path):
print(f"## text_generator_model not found: {text_generator_model_path}, pls download from https://huggingface.co/succinctly/text2image-prompt-generator/tree/main")
text_generator_model_path='succinctly/text2image-prompt-generator'
zh_en_model_path=os.path.join(folder_paths.models_dir, "prompt_generator/opus-mt-zh-en")
zh_en_model_path=os.path.join(prompt_generator, "opus-mt-zh-en")
if not os.path.exists(zh_en_model_path):
print(f"## zh_en_model not found: {zh_en_model_path}, pls download from https://huggingface.co/Helsinki-NLP/opus-mt-zh-en/tree/main")
zh_en_model_path='Helsinki-NLP/opus-mt-zh-en'
def is_installed(package):
try:
spec = importlib.util.find_spec(package)
@@ -274,7 +280,7 @@ class ChinesePrompt:
},
"optional":{
"seed":("INT", {"default": 100, "min": 100, "max": 1000000}),
"seed":("INT", {"default": 100, "min": 100, "max": 0xffffffffffffffff}),
},
@@ -325,13 +331,15 @@ class ChinesePrompt:
for t in texts:
if t:
# translated_text = translated_word = translate(zh_en_tokenizer,zh_en_model,str(t))
parser = Lark(grammar, start="start", parser="lalr", transformer=ChinesePromptTranslate())
# print('t',t)
result = parser.parse(t).children
# print('en_result',result)
# en_text=translate(zh_en_tokenizer,zh_en_model,text_without_syntax)
en_texts.append(result[0])
try:
result = parser.parse(t).children
en_texts.append(result[0])
except:
print(f"Error parsing '{t}'")
t = translate(str(t))
en_texts.append(t)
zh_en_model.to('cpu')
print("test en_text",en_texts)
@@ -378,7 +386,7 @@ class PromptGenerate:
"optional":{
"multiple": (["off","on"],),
"seed":("INT", {"default": 100, "min": 100, "max": 1000000}),
"seed":("INT", {"default": 100, "min": 100, "max": 0xffffffffffffffff}),
},
}
+9 -2
View File
@@ -5,13 +5,20 @@ from PIL import Image
import numpy as np
import torch
from folder_paths import get_filename_list, get_full_path, get_save_image_path, get_output_directory,models_dir
from folder_paths import get_folder_paths, get_full_path, get_save_image_path, get_output_directory,models_dir
from comfy.model_management import get_torch_device
from .tsr.system import TSR
import comfy.utils
triposr_model_path=path.join(models_dir,'triposr/model.ckpt')
def get_triposr_model_path():
try:
return path.join(get_folder_paths('triposr')[0],'model.ckpt')
except:
return path.join(path.join(models_dir, "triposr"),'model.ckpt')
triposr_model_path=get_triposr_model_path()
# Tensor to PIL
+29 -9
View File
@@ -133,7 +133,7 @@ def get_font_files(directory):
return font_files
r_directory = os.path.join(os.path.dirname(__file__), '../assets/')
r_directory = os.path.join(os.path.dirname(__file__), '..','assets','/')
font_files = get_font_files(r_directory)
# print(font_files)
@@ -181,6 +181,28 @@ class ColorInput:
return (h,r,g,b,a,)
class KeyInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"key":("KEY",),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("key",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Input"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,key):
return (key,)
class FontInput:
@classmethod
@@ -284,7 +306,7 @@ class FloatSlider:
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ('weight(0-1)',)
RETURN_NAMES = ('FLOAT',)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Input"
@@ -297,9 +319,7 @@ class FloatSlider:
number = min_value
elif number > max_value:
number = max_value
scaled_number = (number - min_value) / (max_value - min_value)
return (scaled_number,)
return (number,)
class IntNumber:
@classmethod
@@ -568,7 +588,7 @@ class AppInfo:
},
"optional":{
"IMAGE": ("IMAGE",),
"image": ("IMAGE",),
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
"version":("INT", {
"default": 1,
@@ -596,12 +616,12 @@ class AppInfo:
INPUT_IS_LIST = True
# OUTPUT_IS_LIST = (True,)
def run(self,name,input_ids,output_ids,IMAGE,description,version,share_prefix,link,category,auto_save):
def run(self,name,input_ids,output_ids,image,description,version,share_prefix,link,category,auto_save):
name=name[0]
im=None
if IMAGE:
im=IMAGE[0][0]
if image:
im=image[0][0]
#TODO batch 的方式需要处理
im=create_temp_file(im)
# image [img,] img[batch,w,h,a] 列表里面是batch,
+379 -70
View File
@@ -17,9 +17,128 @@ import folder_paths
from comfy.k_diffusion.utils import FolderOfImages
from comfy.utils import common_upscale
import torchaudio
import base64
import mimetypes
def get_frames(frame_count, frames, revert=False):
if not revert:
if frame_count <= len(frames):
return frames[:frame_count]
else:
return [frames[i % len(frames)] for i in range(frame_count)]
else:
extended_frames = frames + frames[-2:0:-1] # 正向加反向中间部分
if frame_count <= len(extended_frames):
return extended_frames[:frame_count]
else:
return [extended_frames[i % len(extended_frames)] for i in range(frame_count)]
# # 示例用法
# frames = ["frame1", "frame2", "frame3"]
# frame_count = 2
# result = get_frames(frame_count, frames, revert=False)
# print(result) # 输出: ['frame1', 'frame2', 'frame3', 'frame1', 'frame2', 'frame3', 'frame1']
# result = get_frames(frame_count, frames, revert=True)
# print(result) # 输出: ['frame1', 'frame2', 'frame3', 'frame2', 'frame1', 'frame2', 'frame3']
def get_mime_type(file_path):
# 获取文件的 MIME 类型
mime_type, _ = mimetypes.guess_type(file_path)
# 如果无法猜测类型,返回默认类型
if mime_type is None:
return 'application/octet-stream'
return mime_type
# import subprocess
# from imageio_ffmpeg import get_ffmpeg_exe
def save_audio_base64s_to_file(base64_audios, output_folder, file_name):
# Ensure the output folder exists
if not os.path.exists(output_folder):
os.makedirs(output_folder)
decoded_audios=[]
for a in base64_audios:
# If the base64 string contains a header, remove it
if ',' in a:
a = a.split(',')[1]
# 解码 base64 数据
a=base64.b64decode(a)
decoded_audios.append(a)
# 拼接音频数据
combined_audio = b''.join(decoded_audios)
# Create the full file path
file_path = os.path.join(output_folder, file_name)
# Write the decoded audio to the file
with open(file_path, 'wb') as audio_file:
audio_file.write(combined_audio)
return file_path
# Example usage
# base64_audio = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAIlYAAESsAAACABAAZGF0YQAAAAA="
# output_folder = "audio_files"
# file_name = "output.wav"
# file_path = save_audio_base64_to_file(base64_audio, output_folder, file_name)
# print(f"Audio saved to: {file_path}")
# 写一个python文件,用来 判断文件夹内命名为 所有chat_tts开头的文件数量(chat_tts_00001),并输出新的编号
def get_new_counter(full_output_folder, filename_prefix):
# 获取目录中的所有文件
files = os.listdir(full_output_folder)
# 过滤出以 filename_prefix 开头并且后续部分为数字的文件
filtered_files = []
for f in files:
if f.startswith(filename_prefix):
# 去掉文件名中的前缀和后缀,只保留中间的数字部分
base_name = f[len(filename_prefix)+1:]
number_part = base_name.split('.')[0] # 假设文件名中只有一个点,即扩展名
if number_part.isdigit():
filtered_files.append(int(number_part))
if not filtered_files:
return 1
# 获取最大的编号
max_number = max(filtered_files)
# 新的编号
return max_number + 1
def crop_audio(input_file, start_time, duration):
# Load the audio file
audio_tensor, sample_rate = torchaudio.load(input_file)
# Convert start_time and duration from seconds to sample indices
start_sample = int(start_time * sample_rate)
end_sample = start_sample + int(duration * sample_rate)
# Perform the slicing
cropped_audio_tensor = audio_tensor[:, start_sample:end_sample]
# Save the cropped audio to a new file
torchaudio.save(input_file, cropped_audio_tensor, sample_rate)
return input_file
def generate_folder_name(directory,video_path):
# Get the directory and filename from the video path
_, filename = os.path.split(video_path)
@@ -60,6 +179,9 @@ def split_video(video_path, video_segment_frames, transition_frames, output_dir)
# 打印当前片段的起始帧和结束帧
print(f"Segment {i+1}: Start Frame {start_frame}, End Frame {end_frame}")
if end_frame<start_frame:
break
# 保存当前片段为一个视频文件
segment_video_path = f"{output_dir}/segment_{i+1}.avi"
@@ -68,6 +190,7 @@ def split_video(video_path, video_segment_frames, transition_frames, output_dir)
segment_video = cv2.VideoWriter(segment_video_path, fourcc, fps, (int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT))))
for frame_num in range(start_frame, end_frame):
ret, frame = video_capture.read()
if ret:
@@ -87,7 +210,7 @@ def split_video(video_path, video_segment_frames, transition_frames, output_dir)
folder_paths.folder_names_and_paths["video_formats"] = (
[
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "video_formats"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), ".", "video_formats"),
],
[".json"]
)
@@ -101,6 +224,25 @@ if ffmpeg_path is None:
except:
print("ffmpeg could not be found. Outputs that require it have been disabled")
def combine_audio_video(audio_path, video_path, output_path):
command = [
ffmpeg_path,
'-i', video_path,
'-i', audio_path,
'-c:v', 'copy',
'-c:a', 'aac',
'-shortest',
output_path
]
subprocess.run(command, check=True)
return output_path
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
@@ -262,7 +404,7 @@ class LoadVideoAndSegment:
files.append(f)
return {"required": {
"video": (sorted(files), {"video_upload": True}),
"video_segment_frames": ("INT", {"default": 10, "min": 1, "step": 1}),
"video_segment_frames": ("INT", {"default": 10, "min": -1, "step": 1}),
"transition_frames": ("INT", {"default": 0, "min": 0, "step": 1}),
},}
@@ -332,63 +474,6 @@ class LoadVideoAndSegment:
video_path = folder_paths.get_annotated_filepath(video)
# check if video is a gif - will need to use cv fallback to read frames
# use cv fallback if ffmpeg not installed or gif
# if ffmpeg_path is None:
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
# otherwise, continue with ffmpeg
# args_dummy = [ffmpeg_path, "-i", video_path, "-f", "null", "-"]
# try:
# with subprocess.Popen(args_dummy, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) as proc:
# for line in proc.stderr.readlines():
# match = re.search(", ([1-9]|\\d{2,})x(\\d+)",line.decode('utf-8'))
# if match is not None:
# size = [int(match.group(1)), int(match.group(2))]
# break
# except Exception as e:
# print(f"Retrying with opencv due to ffmpeg error: {e}")
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
# args_all_frames = [ffmpeg_path, "-i", video_path, "-v", "error",
# "-pix_fmt", "rgb24"]
# vfilters = []
# if skip_first_frames > 0:
# vfilters.append(f"select=gt(n\\,{skip_first_frames-1})")
# if frame_load_cap > 0:
# vfilters.append(f"select=gt({frame_load_cap}\\,n)")
# #manually calculate aspect ratio to ensure reads remain aligned
# if len(vfilters) > 0:
# args_all_frames += ["-vf", ",".join(vfilters)]
# args_all_frames += ["-f", "rawvideo", "-"]
# images = []
# try:
# with subprocess.Popen(args_all_frames, stdout=subprocess.PIPE) as proc:
# #Manually buffer enough bytes for an image
# bpi = size[0]*size[1]*3
# current_bytes = bytearray(bpi)
# current_offset=0
# while True:
# bytes_read = proc.stdout.read(bpi - current_offset)
# if bytes_read is None:#sleep to wait for more data
# time.sleep(.2)
# continue
# if len(bytes_read) == 0:#EOF
# break
# current_bytes[current_offset:len(bytes_read)] = bytes_read
# current_offset+=len(bytes_read)
# if current_offset == bpi:
# images.append(np.array(current_bytes, dtype=np.float32).reshape(size[1], size[0], 3) / 255.0)
# current_offset = 0
# except Exception as e:
# print(f"Retrying with opencv due to ffmpeg error: {e}")
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
# imgs=split_list(images,video_segment_frames,transition_frames)
# temp path
tp=folder_paths.get_temp_directory()
basename = os.path.basename(video_path) # 获取文件名
@@ -396,15 +481,22 @@ class LoadVideoAndSegment:
folder_path = create_folder(tp,name_without_extension)
# 导出的数据
scenes_video,total_frames,fps=split_video(video_path,video_segment_frames,
transition_frames,folder_path)
if video_segment_frames==-1:
# 不切割视频
scenes_video=[video_path]
# 读取视频文件
video_capture = cv2.VideoCapture(video_path)
# 获取视频的总帧数和帧率
total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
fps = video_capture.get(cv2.CAP_PROP_FPS)
else:
# 导出的数据
scenes_video,total_frames,fps=split_video(video_path,video_segment_frames,
transition_frames,folder_path)
# imgs=[torch.from_numpy(np.stack(im)) for im in imgs]
# images = torch.from_numpy(np.stack(images))
return (scenes_video,len(scenes_video), total_frames,fps,)
@@ -422,7 +514,113 @@ class LoadVideoAndSegment:
return "Invalid image file: {}".format(video)
return True
class LoadAndCombinedAudio_:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"audios": ("AUDIOBASE64",),
"start_time": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01}),
"duration": ("FLOAT" , {"default": 10, "min": -1, "max": 10000000, "step": 0.01}),
},
}
CATEGORY = "♾️Mixlab/Audio"
RETURN_TYPES = ("STRING","AUDIO",)
RETURN_NAMES = ("audio_file_path","audio",)
FUNCTION = "run"
def run(self,audios, start_time, duration):
output_dir = folder_paths.get_output_directory()
counter=get_new_counter(output_dir,'audio_')
audio_file_name = f"audio_{counter:05}.wav"
audio_file=save_audio_base64s_to_file(audios['base64'],output_dir,audio_file_name)
# duration == -1 则不裁切
if duration > -1:
crop_audio(audio_file, start_time, duration)
waveform, sample_rate = torchaudio.load(audio_file)
audio = {
"filename": audio_file_name,
"subfolder": "",
"type": "output",
"audio_path":audio_file,
"waveform": waveform.unsqueeze(0),
"sample_rate": sample_rate}
return (audio_file,audio ,)
class CombineAudioVideo:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"video": ("SCENE_VIDEO",),
"audio": ("AUDIO", ),
},
}
CATEGORY = "♾️Mixlab/Video"
OUTPUT_NODE = True
FUNCTION = "run"
RETURN_TYPES = ("SCENE_VIDEO",)
RETURN_NAMES = ("SCENE_VIDEO",)
def run(self,video, audio):
output_dir = folder_paths.get_output_directory()
# 判断是否是 Tensor 类型
is_tensor = not isinstance(audio, dict)
# print('#判断是否是 Tensor 类型',is_tensor,audio)
if not is_tensor and 'waveform' in audio and 'sample_rate' in audio:
# {'waveform': tensor([], size=(1, 1, 0)), 'sample_rate': 44100}
is_tensor=True
if "audio_path" in audio:
is_tensor=False
audio_file_path=audio["audio_path"]
if is_tensor:
filename_prefix="audio_tmp"
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix,
folder_paths.get_temp_directory())
filename_with_batch_num = filename.replace("%batch_num%", str(1))
file = f"{filename_with_batch_num}_{counter:05}_.wav"
audio_file_path=os.path.join(full_output_folder, file)
torchaudio.save(audio_file_path, audio['waveform'].squeeze(0), audio["sample_rate"])
# 获取文件名和扩展名
base, ext = os.path.splitext(video)
counter=get_new_counter(output_dir,'video_final_')
v_file = f"video_final_{counter:05}{ext}"
v_file_path=os.path.join(output_dir, v_file)
combine_audio_video(audio_file_path,video,v_file_path)
previews = [
{
"filename": v_file,
"subfolder": "",
"type": "output",
"format": get_mime_type(v_file),
}
]
return {"ui": {"gifs": previews},"result":(v_file_path,)}
# The code is based on ComfyUI-VideoHelperSuite modification.
class VideoCombine_Adv:
@@ -433,6 +631,7 @@ class VideoCombine_Adv:
ffmpeg_formats = ["video/"+x[:-5] for x in folder_paths.get_filename_list("video_formats")]
else:
ffmpeg_formats = []
# ffmpeg_formats =["video/"+x for x in ['webm', 'mp4', 'mkv']]
return {
"required": {
"image_batch": ("IMAGE",),
@@ -453,7 +652,8 @@ class VideoCombine_Adv:
},
}
RETURN_TYPES = ()
RETURN_TYPES = ("SCENE_VIDEO",)
RETURN_NAMES = ("scenes_video",)
OUTPUT_NODE = True
CATEGORY = "♾️Mixlab/Video"
FUNCTION = "run"
@@ -622,7 +822,7 @@ class VideoCombine_Adv:
"format": format,
}
]
return {"ui": {"gifs": previews}}
return {"ui": {"gifs": previews},"result":(file_path,)}
class VAEEncodeForInpaint_Frames:
@@ -689,4 +889,113 @@ class VAEEncodeForInpaint_Frames:
result.append({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())})
return (result, )
return (result, )
class GenerateFramesByCount:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"frames": ('IMAGE',),
"frame_count": ("INT", {"default": 72, "min": 1, "step": 1}),
"revert" :("BOOLEAN", {"default": True},),
},}
RETURN_TYPES = ('IMAGE',)
RETURN_NAMES = ("frames",)
FUNCTION = "r"
CATEGORY = "♾️Mixlab/Video"
# INPUT_IS_LIST = True
def r(self, frames, frame_count, revert):
image_list = [frames[i:i + 1, ...] for i in range(frames.shape[0])]
image_list=get_frames(frame_count,image_list,revert)
images = torch.cat(image_list, dim=0)
return (images,)
class scenesNode_:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"scenes_video": ('SCENE_VIDEO',),
"index": ("INT", {"default": 0, "min": 0, "step": 1}),
},}
RETURN_TYPES = ('IMAGE','INT',)
RETURN_NAMES = ("video frames (batch)","count",)
# OUTPUT_IS_LIST = (False,)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Video"
INPUT_IS_LIST = True
def load_video_cv_fallback(self, video, frame_load_cap, skip_first_frames):
# print('#video',video)
try:
video_cap = cv2.VideoCapture(video)
if not video_cap.isOpened():
raise ValueError(f"{video} could not be loaded with cv fallback.")
# set video_cap to look at start_index frame
images = []
total_frame_count = 0
frames_added = 0
base_frame_time = 1/video_cap.get(cv2.CAP_PROP_FPS)
target_frame_time = base_frame_time
time_offset=0.0
while video_cap.isOpened():
if time_offset < target_frame_time:
is_returned, frame = video_cap.read()
# if didn't return frame, video has ended
if not is_returned:
break
time_offset += base_frame_time
if time_offset < target_frame_time:
continue
time_offset -= target_frame_time
# if not at start_index, skip doing anything with frame
total_frame_count += 1
if total_frame_count <= skip_first_frames:
continue
# TODO: do whatever operations need to happen, like force_size, etc
# opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
# follow up: can videos ever have an alpha channel?
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# convert frame to comfyui's expected format (taken from comfy's load image code)
image = Image.fromarray(frame)
image = ImageOps.exif_transpose(image)
image = np.array(image, dtype=np.float32) / 255.0
image = torch.from_numpy(image)[None,]
images.append(image)
frames_added += 1
# if cap exists and we've reached it, stop processing frames
if frame_load_cap > 0 and frames_added >= frame_load_cap:
break
finally:
video_cap.release()
images = torch.cat(images, dim=0)
return (images, frames_added,)
def run(self, scenes_video,index):
print('#scenes_video',index,scenes_video)
index=index[0]
if len(scenes_video) > index:
vp=scenes_video[index]
else:
vp=scenes_video[-1]
return self.load_video_cv_fallback(vp,0,0)
+172
View File
@@ -0,0 +1,172 @@
import torch
from PIL import Image, ImageOps, ImageSequence, ImageFile
from PIL.PngImagePlugin import PngInfo
import numpy as np
import os
import folder_paths
import node_helpers
import hashlib
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# tensor 取hash值
def tensor_to_hash(tensor):
# 将 Tensor 转换为 NumPy 数组
np_array = tensor.cpu().numpy()
# 将 NumPy 数组转换为字节数据
byte_data = np_array.tobytes()
# 计算哈希值
hash_value = hashlib.md5(byte_data).hexdigest()
return hash_value
def create_temp_file(image):
output_dir = folder_paths.get_temp_directory()
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('material', output_dir)
image=tensor2pil(image)
image_file = f"{filename}_{counter:05}.png"
image_path=os.path.join(full_output_folder, image_file)
image.save(image_path,compress_level=4)
return (image_path,[{
"filename": image_file,
"subfolder": subfolder,
"type": "temp"
}])
# image - tensor - 文件路径
# loadImage的方法( 文件路径 - image-mask )
class EditMask:
def __init__(self):
self.image_id = None
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ("IMAGE",), # 表示一个张量
},
"optional":{
"image_update": ("IMAGE_FILE",)
},
}
CATEGORY = "♾️Mixlab/Mask"
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image", "mask")
FUNCTION = "edit"
OUTPUT_NODE = True
def edit(self, image,image_update=None):
# 根据image输入来判断是否是新的图片
if self.image_id==None:
self.image_id=tensor_to_hash(image)
image_update=None
else:
image_id=tensor_to_hash(image)
if image_id!=self.image_id:
image_update=None
self.image_id=image_id
image_path=None
# print('#image_update',self.image_id,image_update)
if image_update==None:
print('--')
else:
if 'images' in image_update:
images=image_update['images']
filename=images[0]['filename']
subfolder=images[0]['subfolder']
type=images[0]['type']
name, base_dir=folder_paths.annotated_filepath(filename)
if type.endswith("output"):
base_dir = folder_paths.get_output_directory()
elif type.endswith("input"):
base_dir = folder_paths.get_input_directory()
elif type.endswith("temp"):
base_dir = folder_paths.get_temp_directory()
#base_dir = folder_paths.get_input_directory()
# print(base_dir,subfolder, name)
image_path = os.path.join(base_dir,subfolder, name)
if image_path==None:
image_path,images=create_temp_file(image)
print('#image_path',os.path.exists(image_path),image_path)
# image_path = folder_paths.get_annotated_filepath(image) #文件名
if not os.path.exists(image_path):
image_path,images=create_temp_file(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
# 尺寸不对,需要按照image来
mask = torch.zeros((h, w), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return {"ui":{"images": images},"result": (output_image, output_mask)}
# return (output_image, output_mask)
+10
View File
@@ -0,0 +1,10 @@
{
"main_pass":
[
"-n", "-c:v", "libsvtav1",
"-pix_fmt", "yuv420p10le",
"-crf", "23"
],
"extension": "webm",
"environment": {"SVT_LOG": "1"}
}
+9
View File
@@ -0,0 +1,9 @@
{
"main_pass":
[
"-n", "-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-crf", "19"
],
"extension": "mp4"
}
+11
View File
@@ -0,0 +1,11 @@
{
"main_pass":
[
"-n", "-c:v", "libx265",
"-pix_fmt", "yuv420p10le",
"-preset", "medium",
"-crf", "22",
"-x265-params", "log-level=quiet"
],
"extension": "mp4"
}
+9
View File
@@ -0,0 +1,9 @@
{
"main_pass":
[
"-n",
"-pix_fmt", "yuv420p",
"-crf", "23"
],
"extension": "webm"
}
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "comfyui-mixlab-nodes"
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
version = "0.39.0"
license = "MIT"
dependencies = ["numpy", "pyOpenSSL", "watchdog", "opencv-python-headless", "matplotlib", "openai", "simple-lama-inpainting", "clip-interrogator==0.6.0", "transformers>=4.36.0", "lark-parser", "imageio-ffmpeg", "rembg[gpu]", "omegaconf==2.3.0", "Pillow>=9.5.0", "einops==0.7.0", "trimesh>=4.0.5", "huggingface-hub", "scikit-image"]
[project.urls]
Repository = "https://github.com/shadowcz007/comfyui-mixlab-nodes"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "shadow"
DisplayName = "comfyui-mixlab-nodes"
Icon = ""
+9 -2
View File
@@ -4,7 +4,7 @@ watchdog
opencv-python-headless
matplotlib
openai
simple-lama-inpainting
# simple-lama-inpainting
clip-interrogator==0.6.0
transformers>=4.36.0
lark-parser
@@ -15,4 +15,11 @@ Pillow>=9.5.0
einops==0.7.0
trimesh>=4.0.5
huggingface-hub
scikit-image
scikit-image
torchaudio
soundfile>=0.12.1
json-repair
decord
bitsandbytes
accelerate
+355 -237
View File
@@ -2,6 +2,8 @@ import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
import { loadExternalScript } from './common.js'
const getLocalData = key => {
let data = {}
try {
@@ -26,7 +28,8 @@ const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
async function uploadImage (blob, fileType = '.svg', filename) {
async function uploadImage_ (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
@@ -41,13 +44,17 @@ async function uploadImage (blob, fileType = '.svg', filename) {
// console.log(resp)
let data = await resp.json()
return data
}
async function uploadImage (blob, fileType = '.svg', filename) {
let data = await uploadImage_(blob, fileType, filename)
let { name, subfolder } = data
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
return src
}
@@ -94,7 +101,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -171,6 +181,42 @@ async function changeMaterial (
targetMaterial.pbrMetallicRoughness.baseColorTexture.setTexture(targetTexture)
}
function inputFileClick (isFileURL = false, isGlb = false) {
return new Promise((res, rej) => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
input.accept = isGlb ? '.glb' : 'image/*'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
if (isFileURL) {
res(URL.createObjectURL(file))
return
}
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
input.remove()
res(base64)
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
}
app.registerExtension({
name: 'Mixlab.3D.3DImage',
async getCustomWidgets (app) {
@@ -189,7 +235,7 @@ app.registerExtension({
let d = getLocalData('_mixlab_3d_image')
// console.log('serializeValue', node)
if (d && d[node.id]) {
let { url, bg, material } = d[node.id]
let { url, bg, material, images } = d[node.id]
let data = {}
if (url) {
data.image = await parseImage(url)
@@ -205,6 +251,10 @@ app.registerExtension({
data.material = await parseImage(material)
}
if (images) {
data.images = images
}
return JSON.parse(JSON.stringify(data))
} else {
return {}
@@ -221,6 +271,11 @@ app.registerExtension({
if (nodeType.comfyClass == '3DImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
await loadExternalScript(
'/mixlab/app/lib/model-viewer.min.js',
'module'
)
orig_nodeCreated?.apply(this, arguments)
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
@@ -243,39 +298,29 @@ app.registerExtension({
const inputDiv = (key, placeholder, preview) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'file'
const ip = document.createElement('button')
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
// ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 60%;cursor: pointer;
width: 100px;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
ip.innerText = placeholder
div.appendChild(ip)
let that = this,
filename = new Date().getTime()
let that = this
ip.addEventListener('change', async event => {
const file = event.target.files[0]
const reader = new FileReader()
filename = new Date().getTime()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
ip.addEventListener('click', async event => {
let fileURL = await inputFileClick(true, true)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
oncontextmenu="return false;"
min-field-of-view="0deg" max-field-of-view="180deg"
shadow-intensity="1"
camera-controls
@@ -285,230 +330,303 @@ app.registerExtension({
<div>Variant: <select class="variant"></select></div>
<div>Material: <select class="material"></select></div>
<div>Material: <div class="material_img"> </div></div>
<div><button class="bg">BG</button></div>
<div>
<button class="bg">BG</button>
</div>
<div>
<input class="ddcap_step" type="number" min="1" max="20" step="1" value="1">
<input class="total_images" type="number" min="1" max="180" step="1" value="40">
<input class="ddcap_range" type="range" min="-180" max="180" step="1" value="0">
<input class="ddcap_range_top" type="range" min="-180" max="180" step="1" value="0">
<button class="ddcap">Capture Rotational Screenshots</button></div>
<div><button class="export">Export GLB</button></div>
</div></model-viewer>`
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await uploadImage(blob, '.png')
// console.log(url)
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute(
'data-index'
)
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
bg.addEventListener('click', () => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (!dd[that.id]) dd[that.id] = { url: '', bg: bg_url }
dd[that.id] = {
...dd[that.id],
bg: bg_url,
bg_w: bg_img.naturalWidth,
bg_h: bg_img.naturalHeight
}
setLocalDataOfWin(key, dd)
// 更新尺寸
let w = that.size[0] - 24,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
// 以文本形式读取文件
reader.readAsDataURL(file)
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
const ddcap_step = preview.querySelector('.ddcap_step')
const total_images = preview.querySelector('.total_images')
const ddcap_range = preview.querySelector('.ddcap_range')
const ddcap_range_top = preview.querySelector('.ddcap_range_top')
const ddCap = preview.querySelector('.ddcap')
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
return setTimeout(() => {
res(t)
}, t)
})
}
async function captureImage (isUrl = true) {
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
if (isUrl) return await uploadImage(blob, '.png')
return await uploadImage_(blob, '.png')
}
async function captureImages (angleIncrement = 1, totalImages = 12) {
// 记录初始旋转角度
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
console.log(
'#captureImages',
initialCameraOrbit,
angleIncrement * totalImages
)
// const totalImages = 12
// const angleIncrement = totalRotation / totalImages // Each increment in degrees
let currentAngle =
Number(initialCameraOrbit[0].replace('deg', '')) -
(angleIncrement * totalImages) / 2 // Start from the leftmost angle
let frames = []
modelViewerVariants.removeAttribute('camera-controls')
for (let i = 0; i < totalImages; i++) {
modelViewerVariants.cameraOrbit = `${currentAngle}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
await sleep(1000)
console.log(`Capturing image at angle: ${currentAngle}deg`)
let file = await captureImage(false)
frames.push(file)
currentAngle += angleIncrement
}
await sleep(1000)
// 恢复到初始旋转角度
modelViewerVariants.cameraOrbit = initialCameraOrbit.join(' ')
modelViewerVariants.setAttribute('camera-controls', '')
return frames
}
ddCap.addEventListener('click', async e => {
const angleIncrement = Number(ddcap_step.value),
totalImages = Number(total_images.value)
let images = await captureImages(angleIncrement, totalImages)
// console.log(images)
let dd = getLocalData(key)
dd[that.id].images = images
setLocalDataOfWin(key, dd)
})
ddcap_range.addEventListener('input', async e => {
// console.log(ddcap_range.value)
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
modelViewerVariants.cameraOrbit = `${ddcap_range.value}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
modelViewerVariants.setAttribute('camera-controls', '')
})
ddcap_range_top.addEventListener('input', async e => {
// console.log(ddcap_range.value)
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
modelViewerVariants.cameraOrbit = `${initialCameraOrbit[0]} ${ddcap_range_top.value}deg ${initialCameraOrbit[2]}`
modelViewerVariants.setAttribute('camera-controls', '')
})
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 48}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(modelViewerVariants, selectMaterial, material_img)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await captureImage()
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute('data-index')
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
//更新bg
const updateBgData = (id, key, url, w, h) => {
let dd = getLocalData(key)
// console.log(dd[that.id],url)
if (!dd[id]) dd[id] = { url: '', bg: url }
dd[id] = {
...dd[id],
bg: url,
bg_w: w,
bg_h: h
}
setLocalDataOfWin(key, dd)
}
bg.addEventListener('click', async () => {
//更新bg
updateBgData(that.id, key, '', 0, 0)
preview.style.backgroundImage = 'none'
let base64 = await inputFileClick(false, false)
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
//更新bg
updateBgData(
that.id,
key,
bg_url,
bg_img.naturalWidth,
bg_img.naturalHeight
)
// 更新尺寸
let w = that.size[0] - 48,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 48,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
})
return div
}
let preview = document.createElement('div')
preview.className = 'preview'
preview.style = `margin-top: 12px;display: flex;
preview.style = `margin-top: 12px;
display: flex;
justify-content: center;
align-items: center;background-repeat: no-repeat;background-size: contain;`
align-items: center;background-repeat: no-repeat;
background-size: contain;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
@@ -527,7 +645,7 @@ app.registerExtension({
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
let w = that.size[0] - 48,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
@@ -561,7 +679,7 @@ app.registerExtension({
const r = onExecuted?.apply?.(this, arguments)
let div = this.widgets.filter(d => d.div)[0]?.div
console.log('Test', this.widgets)
// console.log('Test', this.widgets)
let material = message.material[0]
if (material) {
+52 -70
View File
@@ -2,27 +2,13 @@ import { app } from '../../../scripts/app.js'
import { $el } from '../../../scripts/ui.js'
import { api } from '../../../scripts/api.js'
import { td_bg } from './td_background.js'
// console.log('td_bg', td_bg)
import { getUrl, base64Df, get_position_style, getObjectInfo } from './common.js'
//本机安装的插件节点全集
window._nodesAll = null
//获取当前系统的插件,节点清单
function getObjectInfo () {
return new Promise(async (resolve, reject) => {
let url = getUrl()
try {
const response = await fetch(`${url}/object_info`)
const data = await response.json()
resolve(data)
} catch (error) {
reject(error)
}
})
}
const base64Df =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
const parseImageToBase64 = url => {
return new Promise((res, rej) => {
fetch(url)
@@ -42,38 +28,6 @@ const parseImageToBase64 = url => {
})
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 12 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'flex-start'
}
}
async function drawImageToCanvas (imageUrl, sFactor = 320) {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
@@ -184,6 +138,25 @@ async function extractInputAndOutputData (
if (node.type == 'Color') {
}
// 语音输入的支持
if (node.type == 'LoadAndCombinedAudio_') {
// if (
// data[id].widgets_values &&
// data[id].widgets_values[0] &&
// data[id].widgets_values[0].base64 &&
// data[id].widgets_values[0].base64.length > 0
// ) {
// options.defaultBase64 = data[id].widgets_values[0].base64
// }
input[inputIds.indexOf(id)] = {
...data[id],
title: node.title,
id,
options
}
}
if (node.type === 'LoadImage') {
// loadImage的mask支持
let output = node.outputs.filter(ot => ot.type == 'MASK')[0]
@@ -192,7 +165,7 @@ async function extractInputAndOutputData (
options.hasMask = true
}
// loadImage的默认图,转为base64
let imgurl = app.graph.getNodeById(id).imgs[0].src
let imgurl = app.graph.getNodeById(id).imgs[0].src + '&channel=rgb'
options.defaultImage = await drawImageToCanvas(imgurl, 512)
console.log('#loadImage的默认图', options)
@@ -234,7 +207,9 @@ async function extractInputAndOutputData (
node.type === 'KSampler' ||
node.type == 'SamplerCustom' ||
node.type === 'ChinesePrompt_Mix' ||
node.type === 'Seed_'
node.type === 'Seed_' ||
node.type === 'SiliconflowLLM' ||
node.type === 'ChatGPTOpenAI'
) {
// seed 的类型收集
try {
@@ -254,13 +229,6 @@ async function extractInputAndOutputData (
return { input, output, seed, seedTitle }
}
function getUrl () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
const getLocalData = key => {
let data = {}
try {
@@ -396,11 +364,11 @@ async function save (json, download = false, showInfo = true) {
function getInputsAndOutputs () {
const inputs =
`LoadImage LoadImagesToBatch ImagesPrompt_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
`LoadImage LoadImagesToBatch ImagesPrompt_ LoadAndCombinedAudio_ LoadVideoAndSegment_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
' '
),
outputs =
`SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split(
`SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,CombineAudioVideo,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split(
','
)
@@ -444,20 +412,19 @@ app.registerExtension({
const { input, output } = getInputsAndOutputs()
input_ids.value = input.join('\n')
output_ids.value = output.join('\n')
const widget = {
type: 'div',
name: 'AppInfoRun',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(
Object.assign(this.div.style, {
...get_position_style(
ctx,
widget_width,
node.size[1] - widget_height,
node.size[1]
)
)
),
zIndex: 1
})
}
}
@@ -502,6 +469,21 @@ app.registerExtension({
}
})
//td bg
const tdBG = document.createElement('button')
tdBG.innerText = 'Canvas Mode'
tdBG.style = style
tdBG.style.marginLeft = '12px'
tdBG.addEventListener('click', () => {
td_bg.toggle()
if (td_bg.running) {
tdBG.style.background = 'yellow'
} else {
tdBG.style.background = 'transparent'
}
})
// author
let author = document.createElement('div')
// author.style=`display: flex`
@@ -658,6 +640,7 @@ app.registerExtension({
btns.appendChild(btn)
btns.appendChild(download)
btns.appendChild(tdBG)
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
@@ -686,9 +669,8 @@ app.registerExtension({
}
const div = this.widgets.filter(w => w.div)[0].div
Array.from(
div.querySelectorAll('button'),
b => (b.style.background = 'yellow')
Array.from(div.querySelectorAll('button'), b =>
b.innerText != 'Canvas Mode' ? (b.style.background = 'yellow') : ''
)
} catch (error) {}
}
+219 -1
View File
@@ -19,7 +19,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -396,3 +399,218 @@ app.registerExtension({
}
}
})
// 上传音频转为base64
async function uploadAndConvertAudio (file) {
if (!file) {
alert('Please select a WAV file.')
return
}
if (file.type !== 'audio/wav') {
alert('Only WAV files are supported.')
return
}
try {
const base64Audio = await readFileAsDataURL(file)
return base64Audio
} catch (error) {
console.error('Error reading file:', error)
alert('Error reading file.')
}
}
function readFileAsDataURL (file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (event) {
resolve(event.target.result)
}
reader.onerror = function (error) {
reject(error)
}
reader.readAsDataURL(file)
})
}
const createInputAudioForBatch = (base64, widget) => {
// Create an audio element
let audio = document.createElement('audio')
audio.src = base64
audio.controls = true
audio.style = 'width: 120px; display: block'
// Create a delete button
let deleteButton = document.createElement('button')
deleteButton.textContent = 'Delete'
deleteButton.style = `cursor: pointer;
font-weight: 300;
margin: 2px;
margin-left: 10px;
color: var(--descrip-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;height: 30px;min-width: 122px;
`
// Create a container for the audio and delete button
let container = document.createElement('div')
container.appendChild(audio)
container.appendChild(deleteButton)
container.style = `display: flex;margin-top: 12px;`
// Add event listener for the delete button
deleteButton.addEventListener('click', e => {
let newValue = []
let items = widget.value?.base64 || []
for (const v of items) {
if (v != base64) newValue.push(v)
}
widget.value.base64 = newValue
container.remove()
})
return container
}
app.registerExtension({
name: 'Mixlab.Comfy.LoadAndCombinedAudio_',
async getCustomWidgets (app) {
return {
AUDIOBASE64 (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
value: {
base64: []
}, // 不能[x,x,x]
type: inputData[0], // the type
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 122] // a method to compute the current size of the widget
}
// serializeValue (nodeId, widgetIndex) {
// return widget.value
// },
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'LoadAndCombinedAudio_') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
let audiosWidget = this.widgets.filter(w => w.name == 'audios')[0]
const widget = {
type: 'div',
name: 'audio_base64',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
)
},
serialize: false
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
let audioPreview = document.createElement('div')
let audiosDiv = document.createElement('div') //显示图片
audiosDiv.className = 'audios_preview'
audiosDiv.style = `width: calc(100% - 14px);
display: flex;
flex-wrap: wrap;
padding: 7px; justify-content: space-between;
align-items: center;`
const btn = document.createElement('button')
btn.innerText = 'Upload Audio'
btn.style = `cursor: pointer;
font-weight: 300;
margin: 2px;
color: var(--descrip-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;height: 30px;min-width: 122px;
`
btn.addEventListener('click', e => {
e.preventDefault()
let inputAudio = document.createElement('input')
inputAudio.type = 'file'
inputAudio.accept = "audio/*"
inputAudio.style.display = 'none'
inputAudio.addEventListener('change', async e => {
e.preventDefault()
const file = e.target.files[0]
let base64 = await uploadAndConvertAudio(file)
if (!audiosWidget.value) audiosWidget.value = { base64: [] }
audiosWidget.value.base64.push(base64)
let a = createInputAudioForBatch(base64, audiosWidget)
audiosDiv.appendChild(a)
})
inputAudio.click()
inputAudio.remove()
})
widget.div.appendChild(audioPreview)
audioPreview.appendChild(audiosDiv)
audioPreview.appendChild(btn)
// audioPreview.appendChild(inputAudio)
this.addCustomWidget(widget)
// document.addEventListener('wheel', handleMouseWheel)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
try {
// document.removeEventListener('wheel', handleMouseWheel)
} catch (error) {
console.log(error)
}
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'LoadAndCombinedAudio_') {
// await sleep(0)
let audiosWidget = node.widgets.filter(w => w.name === 'audios')[0]
let audioPreview = node.widgets.filter(w => w.name == 'audio_base64')[0]
let pre = audioPreview.div.querySelector('.audios_preview')
for (const d of audiosWidget.value?.base64 || []) {
let im = createInputAudioForBatch(d, audiosWidget)
pre.appendChild(im)
}
}
}
})
+81 -16
View File
@@ -1,8 +1,10 @@
import { getUrl } from './common.js'
async function* completion (url, messages, controller) {
let data = {
model: 'gpt-3.5-turbo-16k',
messages,
temperature: 0.6,
temperature: 0.05,
stream: true
}
// if (imageNode) {
@@ -36,7 +38,6 @@ async function* completion (url, messages, controller) {
break
}
// Add any leftover data to the current chunk of data
const text = leftover + decoder.decode(result.value)
@@ -64,14 +65,14 @@ async function* completion (url, messages, controller) {
if (result.data) {
result.data = JSON.parse(result.data)
// console.log('#result.data',result.data)
content += result.data.choices[0].delta?.content||''
content += result.data.choices[0].delta?.content || ''
// yield
yield result
// if we got a stop token from server, we will break here
if (result.data.choices[0].finish_reason=="stop") {
if (result.data.choices[0].finish_reason == 'stop') {
if (result.data.generation_settings) {
// generation_settings = result.data.generation_settings;
}
@@ -92,17 +93,81 @@ async function* completion (url, messages, controller) {
return content
// return (await response.json()).content
}
export async function completion_ (url, messages, controller, callback) {
let request = await completion(url, messages, controller)
export async function completion_ (apiKey, url, messages, controller, callback) {
let request = await chatCompletion(apiKey, url, messages, controller)
for await (const chunk of request) {
let content=chunk.data.choices[0].delta.content||""
if(chunk.data.choices[0].role=="assistant"){
//开始
content=""
}
if (callback) callback(content)
if (callback) callback(chunk)
}
}
export async function* chatCompletion (apiKey, url, messages, controller) {
url = `${getUrl()}/chat/completions`
const requestBody = {
model: '01-ai/Yi-1.5-9B-Chat-16K',
messages: messages,
stream: true,
key: apiKey
}
let response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify(requestBody),
mode: 'cors', // This is to ensure the request is made with CORS
signal: controller.signal
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let content = ''
let leftover = '' // Buffer for partially read lines
try {
let cont = true
while (cont) {
let result = await reader.read()
if (result.done) {
break
}
// Add any leftover data to the current chunk of data
const text = leftover + decoder.decode(result.value)
// Check if the last character is a line break
const endsWithLineBreak = text.endsWith('\r\n')
// Split the text into lines
let lines = text.split('\r\n')
// If the text doesn't end with a line break, then the last line is incomplete
// Store it in leftover to be added to the next chunk of data
if (!endsWithLineBreak) {
leftover = lines.pop()
} else {
leftover = '' // Reset leftover if we have a line break at the end
}
for (const line of lines) {
if (line) {
content += line
yield line // Yield the trimmed line
} else {
cont = false
break
}
}
}
} catch (e) {
console.error('chat error: ', e)
throw e
} finally {
controller.abort()
}
return content
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.25.0'
const version = 'v0.39.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+175
View File
@@ -0,0 +1,175 @@
export const base64Df =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
export function getUrl () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
// 更新或者获取key
export const updateLLMAPIKey = async key => {
try {
const res = await fetch(`${getUrl()}/mixlab/llm_api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
key: key || null
})
})
const data = await res.json()
if (!res.ok) {
console.error('Error:', data.error)
return
}
if (key) {
console.log('API key saved successfully:', data.message)
return key
} else {
console.log('Retrieved API key:', data.key)
return data.key
}
} catch (error) {
console.error('Request failed:', error)
}
}
//获取当前系统的插件,节点清单
export function getObjectInfo () {
return new Promise(async (resolve, reject) => {
let url = getUrl()
try {
const response = await fetch(`${url}/object_info`)
const data = await response.json()
resolve(data)
} catch (error) {
reject(error)
}
})
}
export function get_position_style (
ctx,
widget_width,
y,
node_height,
left = 44
) {
const MARGIN = 0 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const scaleX = elRect.width / ctx.canvas.width
const scaleY = elRect.height / ctx.canvas.height
const transform = new DOMMatrix()
.scaleSelf(scaleX, scaleY)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `${left}px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'flex-start',
zIndex: 99
}
}
export function loadExternalScript (url, type) {
return new Promise((resolve, reject) => {
const existingScript = document.querySelector(`script[src="${url}"]`)
if (existingScript) {
existingScript.onload = () => {
resolve()
}
existingScript.onerror = reject
return
}
const script = document.createElement('script')
script.src = url
if (type) script.type = type // Add this line to load the script as an ES module
script.onload = () => {
resolve()
}
script.onerror = reject
document.head.appendChild(script)
})
}
export async function getQueue () {
try {
const res = await fetch(`${getUrl()}/queue`)
const data = await res.json()
// console.log(data.queue_running,data.queue_pending)
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.length,
Pending: data.queue_pending.length
}
} catch (error) {
console.error(error)
return { Running: 0, Pending: 0 }
}
}
export async function interrupt () {
const resp = await fetch(`${getUrl()}/interrupt`, {
method: 'POST'
})
}
export async function sleep (t = 200) {
return new Promise((res, rej) => {
setTimeout(() => {
res(true)
}, t)
})
}
export function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
export const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
export const saveLocalData = (key, id, val) => {
let data = getLocalData(key)
data[id] = val
localStorage.setItem(key, JSON.stringify(data))
}
+8 -203
View File
@@ -1,205 +1,5 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
async function getConfig () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const res = await fetch(`${url}/mixlab`, {
method: 'POST'
})
return await res.json()
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
app.registerExtension({
name: 'Mixlab.GPT.ChatGPTOpenAI',
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('##inputData', inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
URL (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_url')
return data[node.id] || 'https://api.openai.com/v1'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'ChatGPTOpenAI') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const api_key = this.widgets.filter(w => w.name == 'api_key')[0]
const api_url = this.widgets.filter(w => w.name == 'api_url')[0]
console.log('ChatGPTOpenAI nodeData', this.widgets)
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, api_key.y, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = placeholder === 'Key' ? 'password' : 'text'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = placeholder
ip.style = `margin-left: 24px;
outline: none;
border: none;
padding: 4px;width: 100%;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
console.log(this.id, key)
})
return div
}
let inputKey = inputDiv('_mixlab_api_key', 'Key')
let inputUrl = inputDiv('_mixlab_api_url', 'URL')
widget.div.appendChild(inputKey)
widget.div.appendChild(inputUrl)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputUrl.remove()
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'ChatGPTOpenAI') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_api_key'),
url = getLocalData('_mixlab_api_url')
let id = node.id
// console.log('ChatGPTOpenAI serialize_widgets', this)
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
widget.div.querySelector('.URL').value =
url[id] || 'https://api.openai.com/v1'
}
}
})
app.registerExtension({
name: 'Mixlab.GPT.ShowTextForGPT',
@@ -209,13 +9,16 @@ app.registerExtension({
text = text.filter(t => t && t?.trim())
if (this.widgets) {
// console.log('#ShowTextForGPT',this.widgets)
// const pos = this.widgets.findIndex(w => w.name === 'text')
for (let i = 0; i < this.widgets.length; i++) {
if (this.widgets[i].name == 'show_text') this.widgets[i].onRemove?.()
if (this.widgets[i].name == 'show_text')
this.widgets[i].onRemove?.()
}
this.widgets.length = 1
this.widgets.length = 2
}
// console.log('ShowTextForGPT',text)
for (let list of text) {
if (list) {
// console.log('#####', list)
@@ -228,6 +31,8 @@ app.registerExtension({
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
// w.inputEl.style.display='none'
try {
if (typeof list != 'string') {
let data = JSON.parse(list)
+222 -38
View File
@@ -4,6 +4,8 @@ import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
import { applyTextReplacements } from '../../../scripts/utils.js'
import { loadExternalScript, get_position_style } from './common.js'
function loadImageToCanvas (base64Image) {
var img = new Image()
var canvas = document.createElement('canvas')
@@ -88,37 +90,40 @@ function getContentTypeFromBase64 (base64Data) {
// const blob = base64ToBlob(base64Data, contentType);
// console.log(blob);
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
// function get_position_style (ctx, widget_width, y, node_height) {
// const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
// /* Create a transform that deals with all the scrolling and zooming */
// const elRect = ctx.canvas.getBoundingClientRect()
// const transform = new DOMMatrix()
// .scaleSelf(
// elRect.width / ctx.canvas.width,
// elRect.height / ctx.canvas.height
// )
// .multiplySelf(ctx.getTransform())
// .translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
// return {
// transformOrigin: '0 0',
// transform: transform,
// left:
// document.querySelector('.comfy-menu').style.display === 'none'
// ? `60px`
// : `0`,
// top: `0`,
// cursor: 'pointer',
// position: 'absolute',
// maxWidth: `${widget_width - MARGIN * 2}px`,
// // maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
// width: `${widget_width - MARGIN * 2}px`,
// // height: `${node_height * 0.3 - MARGIN * 2}px`,
// // background: '#EEEEEE',
// display: 'flex',
// flexDirection: 'column',
// // alignItems: 'center',
// justifyContent: 'space-around'
// }
// }
const getLocalData = key => {
let data = {}
@@ -344,7 +349,7 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
get_position_style(ctx, widget_width, 44, node.size[1], 36)
)
}
}
@@ -530,7 +535,7 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, y, node.size[1])
get_position_style(ctx, widget_width, y, node.size[1], 36)
)
}
}
@@ -675,6 +680,18 @@ const createInputImageForBatch = (base64, widget) => {
return im
}
// 添加新图片
const addBase64ToWidgetForLoadImagesToBatch = (
base64,
imagesWidget,
imagesDiv
) => {
if (!imagesWidget.value.base64) imagesWidget.value.base64 = []
imagesWidget.value.base64.push(base64)
let im = createInputImageForBatch(base64, imagesWidget)
imagesDiv.appendChild(im)
}
app.registerExtension({
name: 'Mixlab.Comfy.LoadImagesToBatch',
async getCustomWidgets (app) {
@@ -706,6 +723,7 @@ app.registerExtension({
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'LoadImagesToBatch') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
@@ -717,7 +735,7 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
get_position_style(ctx, widget_width, 44, node.size[1], 44)
)
},
serialize: false
@@ -749,13 +767,18 @@ app.registerExtension({
base64 = await loadImageToCanvas(base64)
// console.log(base64)
if (!imagesWidget.value) imagesWidget.value = { base64: [] }
imagesWidget.value.base64.push(base64)
let im = createInputImageForBatch(base64, imagesWidget)
imagesDiv.appendChild(im)
addBase64ToWidgetForLoadImagesToBatch(
base64,
imagesWidget,
imagesDiv
)
}
reader.readAsDataURL(file)
})
// 如果是复制的,有数据 , 这个不生效,取不到数据, 需要在nodeCreated里获取
// console.log('#LoadImagesToBatch', imagesWidget.value?.base64)
const btn = document.createElement('button')
btn.innerText = 'Upload Image'
@@ -827,17 +850,178 @@ app.registerExtension({
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'LoadImagesToBatch') {
// await sleep(0)
let imagesWidget = node.widgets.filter(w => w.name === 'images')[0]
let imagePreview = node.widgets.filter(w => w.name == 'image_base64')[0]
// console.log('#LoadImagesToBatch', imagesWidget.value?.base64)
let imagesDiv = imagePreview.div.querySelector('.images_preview')
let pre = imagePreview.div.querySelector('.images_preview')
for (const d of imagesWidget.value?.base64 || []) {
let im = createInputImageForBatch(d, imagesWidget)
pre.appendChild(im)
imagesDiv.appendChild(im)
}
}
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
// console.log('#LoadImagesToBatch', node.type)
if (node.type === 'LoadImagesToBatch') {
let imagesWidget = node.widgets.filter(w => w.name === 'images')[0]
let imagePreview = node.widgets.filter(w => w.name == 'image_base64')[0]
let imagesDiv = imagePreview?.div?.querySelector('.images_preview')
for (const d of imagesWidget.value?.base64 || []) {
let im = createInputImageForBatch(d, imagesWidget)
imagesDiv.appendChild(im)
}
}
}, 1000)
}
})
// 如何引入css
app.registerExtension({
name: 'Mixlab.output.ComparingTwoFrames_',
init () {
loadExternalScript('/mixlab/app/lib/juxtapose.min.js')
$el('link', {
rel: 'stylesheet',
href: '/mixlab/app/lib/juxtapose.css',
parent: document.head
})
$el('style', {
textContent: `
.juxtapose-name{
display: none!important;
}
`,
parent: document.body
})
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'ComparingTwoFrames_') {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined
this.size = [400, this.size[1]]
console.log('##onNodeCreated', this)
const widget = {
type: 'div',
name: 'preview',
draw (ctx, node, widget_width, y, widget_height) {
let s = get_position_style(ctx, widget_width, 44, node.size[1], 36)
delete s.height
Object.assign(this.div.style, s)
},
serialize: false
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
this.serialize_widgets = true //需要保存参数
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
return r
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
console.log('##onExecuted', this, message)
this.widgets[0].div.id = 'mix_comparingtowframes_' + this.id
let after_image = message.after_images[0]
let before_image = message.before_images[0]
after_image = `${window.location.protocol}//${
window.location.hostname
}:${window.location.port}/view?filename=${encodeURIComponent(
after_image.filename
)}&type=${after_image.type}&subfolder=${encodeURIComponent(
after_image.subfolder
)}&t=${+new Date()}`
before_image = `${window.location.protocol}//${
window.location.hostname
}:${window.location.port}/view?filename=${encodeURIComponent(
before_image.filename
)}&type=${before_image.type}&subfolder=${encodeURIComponent(
before_image.subfolder
)}&t=${+new Date()}`
this.widgets[0].div.innerHTML = ''
let slider = new juxtapose.JXSlider(
'#mix_comparingtowframes_' + this.id,
[
{
src: before_image,
label: 'Before'
},
{
src: after_image,
label: 'After'
}
],
{
animate: true,
showLabels: true,
showCredits: false,
startingPosition: '50%',
makeResponsive: false
}
)
this.widgets_values = [
{
src: before_image,
label: 'Before'
},
{
src: after_image,
label: 'After'
}
]
this.size = [this.size[0], 300]
}
}
},
async loadedGraphNode (node, app) {
// console.log('##loadedGraphNode', node)
if (node.type === 'ComparingTwoFrames_') {
// node.widgets[0].div.id = 'mix_comparingtowframes_' + node.id
// if (node.widgets_values && node.widgets_values[0]) {
// node.widgets[0].div.innerHTML = ''
// let slider = new juxtapose.JXSlider(
// '#mix_comparingtowframes_' + node.id,
// node.widgets_values,
// {
// animate: true,
// showLabels: true,
// showCredits: false,
// startingPosition: '50%',
// makeResponsive: false
// }
// )
// }
}
}
})
+4 -1
View File
@@ -81,7 +81,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
+16 -88
View File
@@ -3,31 +3,17 @@ import { app } from '../../../scripts/app.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
import {
getQueue,
interrupt,
get_position_style,
base64Df,
getUrl,
createImage,
sleep
} from './common.js'
async function getQueue () {
try {
const res = await fetch(`${url}/queue`)
const data = await res.json()
// console.log(data.queue_running,data.queue_pending)
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.length,
Pending: data.queue_pending.length
}
} catch (error) {
console.error(error)
return { Running: 0, Pending: 0 }
}
}
async function interrupt () {
const resp = await fetch(`${url}/interrupt`, {
method: 'POST'
})
}
// let url = getUrl()
async function clipboardWriteImage (win, url) {
const canvas = document.createElement('canvas')
@@ -208,22 +194,6 @@ async function shareScreen (
}
}
async function sleep (t = 200) {
return new Promise((res, rej) => {
setTimeout(() => {
res(true)
}, t)
})
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
async function compareImages (threshold, previousImage, currentImage) {
// 将 base64 转换为 Image 对象
var previousImg = await createImage(previousImage)
@@ -458,44 +428,6 @@ async function requestCamera () {
return false
}
/*
A method that returns the required style for the html
*/
function get_position_style (ctx, widget_width, y, node_height, top) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `${top}px`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const base64Df =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
app.registerExtension({
name: 'Mixlab.image.ScreenShareNode',
async getCustomWidgets (app) {
@@ -593,17 +525,12 @@ app.registerExtension({
type: 'HTML', // whatever
name: 'sreen_share', // whatever
draw (ctx, node, widget_width, y, widget_height) {
// console.log('ScreenSHare', y, widget_height)
// console.log('ScreenSHare', node)
Object.assign(
this.card.style,
get_position_style(
ctx,
widget_width,
widget_height * 5,
node.size[1],
40
)
get_position_style(ctx, widget_width, y, node.size[1], 40)
)
}
}
@@ -1043,12 +970,13 @@ async function setArea (src) {
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
height: 100vh;
z-index:999999;
z-index:99999999999999;
width: 100%;'>
<img id='ml_video' style='position: absolute;
height: ${displayHeight}px;user-select: none;
-webkit-user-drag: none;
outline: 2px solid #eaeaea;
left: 0;
box-shadow: 8px 9px 17px #575757;' />
<div id='ml_selection' style='position: absolute;
border: 2px dashed red;
@@ -1267,7 +1195,7 @@ app.registerExtension({
})
widget.PictureInPicture = $el('button', {
innerText: 'PictureInPicture',
innerText: 'Picture In Picture',
style: {
display: 'pictureInPictureEnabled' in document ? 'block' : 'none',
cursor: 'pointer',
+195
View File
@@ -0,0 +1,195 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
import { get_position_style } from './common.js'
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
let { name, subfolder } = data
// let src = api.apiURL(
// `/view?filename=${encodeURIComponent(
// name
// )}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
// )
return data
}
// 上传得到url
async function uploadBase64ToFile (base64) {
let bg_blob = await base64ToBlobFromURL(base64)
let url = await uploadImage(bg_blob, '.png')
return url
}
const p5InputNode = {
name: 'Mixlab.Comfy.P5Input',
async getCustomWidgets (app) {
return {
IMAGEBASE64 (node, inputName, inputData, app) {
const widget = {
value: {
images: []
}, // 不能[x,x,x]
type: inputData[0], // the type
name: inputName, // the name, slice
size: [320, 120], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
}
}
node.addCustomWidget(widget)
return widget
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'P5Input') {
// console.log('P5Input')
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const widget = {
type: 'div',
name: 'image_base64',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(
ctx,
widget_width - 24,
44,
node.size[1] * 2.8,
44
)
)
},
serialize: false
}
widget.div = $el('div', {})
widget.div.style = `margin:12px;width:400px;height:480px;background:white`
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
// document.addEventListener('wheel', handleMouseWheel)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
// window.removeEventListener('message', ms)
return onRemoved?.()
}
// 节点的大小控制
this.setSize([480, 560])
app.canvas.draw(true, true)
const onResize = this.onResize
this.onResize = () => {
// 设置最小尺寸
if (
Math.max(this.size[0], 480) != this.size[0] &&
Math.max(this.size[1], 560) != this.size[1]
) {
this.setSize([
Math.max(this.size[0], 480),
Math.max(this.size[1], 560)
])
}
return onResize?.apply(this, arguments)
}
this.serialize_widgets = true //需要保存参数
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
// console.log('##onExecuted', this, message._info)
// app.graph.getNodeById(8).widgets[1].div.querySelector('iframe').contentWindow.postMessage('Hello from parent', '*');
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'P5Input') {
}
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
let widget = node.widgets?.filter(w => w.name == 'image_base64')[0]
let framesWidget = node.widgets?.filter(w => w.name == 'frames')[0]
if (node.type === 'P5Input' && widget) {
console.log('#nodeCreated P5Input')
if (framesWidget && !framesWidget.value)
framesWidget.value = { images: [] }
framesWidget.value._seed = Math.random()
let nodeId = node.id
//延迟才能获得this.id
widget.div.innerHTML = `<iframe src="mixlab/app/p5_export/p5.html?id=${nodeId}"
style="border:0;width:100%;height:100%;"
></iframe>`
// 监听来自iframe的消息
const ms = async event => {
const data = event.data
console.log('#P5 Input #', data)
if (
data.from === 'p5.widget' &&
data.status === 'save' &&
data.frames &&
data.frames.length >= 0 &&
data.nodeId == nodeId &&
data.id != framesWidget.value.id
) {
const frames = data.frames
//workflow会存储到local,会卡死
framesWidget.value.images = []
for (const f of frames) {
let file = await uploadBase64ToFile(f)
framesWidget.value.images.push(file)
}
// framesWidget.value.base64 = frames
// framesWidget.value._seed = Math.random()
node.title = 'P5 Input #' + frames.length
framesWidget.value.id = data.id
}
}
window.addEventListener('message', ms)
}
}, 1000)
}
}
app.registerExtension(p5InputNode)
+17 -10
View File
@@ -3,7 +3,7 @@ import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import PhotoSwipeLightbox from '/extensions/comfyui-mixlab-nodes/lib/photoswipe-lightbox.esm.min.js'
import PhotoSwipeLightbox from '/mixlab/app/lib/photoswipe-lightbox.esm.min.js'
function loadCSS (url) {
var link = document.createElement('link')
link.rel = 'stylesheet'
@@ -40,14 +40,14 @@ function loadCSS (url) {
// Append the style element to the document head
document.head.appendChild(style)
}
loadCSS('/extensions/comfyui-mixlab-nodes/lib/photoswipe.min.css')
loadCSS('/mixlab/app/lib/photoswipe.min.css')
function initLightBox () {
const lightbox = new PhotoSwipeLightbox({
gallery: '.prompt_image_output',
children: 'a',
pswpModule: () =>
import('/extensions/comfyui-mixlab-nodes/lib/photoswipe.esm.min.js')
import('/mixlab/app/lib/photoswipe.esm.min.js')
})
lightbox.on('uiRegister', function () {
@@ -100,7 +100,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -178,7 +181,7 @@ app.registerExtension({
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const mutable_prompt = this.widgets.filter(
w => w.name == 'mutable_prompt'
)[0]
@@ -190,7 +193,12 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, y, node.size[1])
get_position_style(
ctx,
widget_width,
y + widget_height + 24,
node.size[1]
)
)
}
}
@@ -207,7 +215,7 @@ app.registerExtension({
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid; height: 30px;min-width: 122px;
border-style: solid;height: 30px;min-width: 122px;
`
// const btn=document.createElement('button');
@@ -266,7 +274,6 @@ app.registerExtension({
},
async loadedGraphNode (node, app) {
if (node.type === 'RandomPrompt') {
}
}
})
@@ -408,7 +415,7 @@ const _createResult = async (node, widget, message) => {
const width = node.size[0] * 0.5 - 12
let height_add = 0
for (let index = 0; index < message._images.length; index++) {
const imgs = message._images[index]
@@ -559,7 +566,7 @@ app.registerExtension({
let cards = widget.div.querySelectorAll('.card')
if (cards.length == 0) node.size = [280, 120]
if(widget.value) _createResult(node, widget, widget.value)
if (widget.value) _createResult(node, widget, widget.value)
}
}
})
+4 -1
View File
@@ -19,7 +19,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
+298
View File
@@ -0,0 +1,298 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: '0',
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
//把文件转为url访问
const parseUrl = data => {
let { filename, subfolder, type, prompt } = data
return {
url: api.apiURL(
`/view?filename=${encodeURIComponent(
filename
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
),
prompt
}
}
const createWaveSurfer = (wavesurfer, id,url) => {
// Create an instance of WaveSurfer
if (wavesurfer) {
wavesurfer.destroy()
}
wavesurfer = WaveSurfer.create({
container: '#' + id,
waveColor: 'rgb(200, 0, 200)',
progressColor: 'rgb(100, 0, 100)',
// Set a bar width
barWidth: 10,
// Optionally, specify the spacing between bars
barGap: 2,
// And the bar radius
barRadius: 6,
url
})
wavesurfer._auto = true
// 监听播放结束事件,重新开始播放以实现循环播放
wavesurfer.on('finish', function () {
// console.log(wavesurfer)
if (wavesurfer._auto) wavesurfer.play()
})
wavesurfer.on('interaction', () => {
wavesurfer._auto = false
if (!wavesurfer.isPlaying()) wavesurfer.play()
})
// 获取当前播放时间的峰值
wavesurfer.on('audioprocess', () => {
if (wavesurfer.isPlaying()&&wavesurfer.getDecodedData()) {
const channelData = wavesurfer.getDecodedData().getChannelData(0);
const currentTime = wavesurfer.getCurrentTime()
// console.log(wavesurfer)
const sampleRate = wavesurfer.getDecodedData().sampleRate
// 定义要分析的时间窗口(例如1秒)
const windowSize = 1
const startSample = Math.floor(currentTime * sampleRate)
const endSample = Math.min(
startSample + windowSize * sampleRate,
channelData.length
)
let peak = 0
for (let i = startSample; i < endSample; i++) {
const value = Math.abs(channelData[i])
if (value > peak) {
peak = value
}
}
// console.log('Current Peak:', peak)
}
})
return wavesurfer
}
//更新gui
function updateWaveWidgetValue (widgets, id, url, prompt, wavesurfer) {
let widget = widgets.filter(w => w.name == 'AudioPlay')[0]
// 手动更新widget值
widget.value = [url, prompt]
if (widget.div) {
widget.div.querySelector('.wave').id = `AudioPlay_${id}`
}
wavesurfer = createWaveSurfer(wavesurfer, `AudioPlay_${id}`,url)
wavesurfer.on('ready', duration => {
console.log('Audio duration: ' + duration + ' seconds')
if (widget.div) {
widget.div.setAttribute('data-url', url)
widget.div.querySelector('.link').setAttribute('href', url)
widget.div.querySelector(
'.info'
).innerHTML = `<span style="font-size: 12px;
margin: 8px;">${duration.toFixed(
2
)} seconds</span> <br><span style="font-size: 14px;">${prompt||''}</span> <br>`
}
})
wavesurfer.load(url)
// console.log('updateWaveWidgetValue' ,url,wavesurfer)
return wavesurfer
}
app.registerExtension({
name: 'SoundLab.AudioPlay',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'AudioPlay') {
let that = this
// console.log('that', that)
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const widget = {
type: 'div',
name: 'AudioPlay',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, y, node.size[1])
)
}
}
// console.log('AudioPlay nodeData', this)
widget.div = $el('div', {})
document.body.appendChild(widget.div)
// wave
const waveDiv = document.createElement('div')
waveDiv.className = 'wave'
waveDiv.style.minHeight = '172px'
widget.div.appendChild(waveDiv)
//prompt 相关信息展示
const infoDiv = document.createElement('div')
infoDiv.className = 'info'
infoDiv.style.marginBottom = '20px'
widget.div.appendChild(infoDiv)
// 按钮的区域
let btns = document.createElement('div')
btns.className = 'btns'
btns.style = `display: flex;
width: 100%;
justify-content: space-between;`
widget.div.appendChild(btns)
//play button
const playBtn = document.createElement('a')
playBtn.innerText = 'Play/Pause'
playBtn.style = `
display: flex;
padding: 4px 15px;
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
color: var(--descrip-text);
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s ease 0s;
`
playBtn.addEventListener('click', e => {
e.preventDefault()
if (that[`wavesurfer_${this.id}`]) {
that[`wavesurfer_${this.id}`]?.playPause()
that[`wavesurfer_${this.id}`]._auto = true
}
})
btns.appendChild(playBtn)
const urlLink = document.createElement('a')
urlLink.className = 'link'
urlLink.innerText = 'URL'
urlLink.setAttribute('target', '_blank')
urlLink.style = `display: flex;
padding: 4px 15px;
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
color: var(--descrip-text);
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s ease 0s;`
// urlLink.style.minHeight = '200px'
btns.appendChild(urlLink)
//todo 导出视频 that[`wavesurfer_${this.id}`].renderer.exportImage('image/png',1,'dataURL')
// https://github.com/diffusion-studio/ffmpeg-js
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
this.size = [this.size[0], 280]
this.serialize_widgets = true //需保存widget的值
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
const audio = message.audio
console.log('#onExecuted', `AudioPlay_${this.id}`, message,audio)
try {
let { url, prompt } = parseUrl(audio[0])
that[`wavesurfer_${this.id}`] = updateWaveWidgetValue(
this.widgets,
this.id,
url,
prompt,
that[`wavesurfer_${this.id}`]
)
that[`wavesurfer_${this.id}`]?.playPause()
} catch (error) {
console.log(error)
}
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'AudioPlay') {
let widget = node.widgets.filter(w => w.name == 'AudioPlay')[0]
if (widget.value) {
let [url, prompt] = widget.value
this[`wavesurfer_${node.id}`] = updateWaveWidgetValue(
node.widgets,
node.id,
url,
prompt,
this[`wavesurfer_${node.id}`]
)
}
console.log('#loadedGraphNode', node)
}
}
})
+323
View File
@@ -0,0 +1,323 @@
// touchdesigner的背景效果,把appinfo的输出,选择一张图片作为背景
window._bg_img = null
/**
* draws the back canvas (the one containing the background and the connections)
* @method drawBackCanvas
**/
LGraphCanvas.prototype.drawBackCanvas = function () {
var canvas = this.bgcanvas
if (
canvas.width != this.canvas.width ||
canvas.height != this.canvas.height
) {
canvas.width = this.canvas.width
canvas.height = this.canvas.height
}
if (!this.bgctx) {
this.bgctx = this.bgcanvas.getContext('2d')
}
var ctx = this.bgctx
if (ctx.start) {
ctx.start()
}
var viewport = this.viewport || [0, 0, ctx.canvas.width, ctx.canvas.height]
//clear
if (this.clear_background) {
ctx.clearRect(viewport[0], viewport[1], viewport[2], viewport[3])
}
//show subgraph stack header
if (this._graph_stack && this._graph_stack.length) {
ctx.save()
var parent_graph = this._graph_stack[this._graph_stack.length - 1]
var subgraph_node = this.graph._subgraph_node
ctx.strokeStyle = subgraph_node.bgcolor
ctx.lineWidth = 10
ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2)
ctx.lineWidth = 1
ctx.font = '40px Arial'
ctx.textAlign = 'center'
ctx.fillStyle = subgraph_node.bgcolor || '#AAA'
var title = ''
for (var i = 1; i < this._graph_stack.length; ++i) {
title += this._graph_stack[i]._subgraph_node.getTitle() + ' >> '
}
ctx.fillText(title + subgraph_node.getTitle(), canvas.width * 0.5, 40)
ctx.restore()
}
var bg_already_painted = false
if (this.onRenderBackground) {
bg_already_painted = this.onRenderBackground(canvas, ctx)
}
//reset in case of error
if (!this.viewport) {
ctx.restore()
ctx.setTransform(1, 0, 0, 1, 0, 0)
}
this.visible_links.length = 0
if (this.graph) {
//apply transformations
ctx.save()
this.ds.toCanvasContext(ctx)
//render BG
if (
this.ds.scale < 1 &&
!bg_already_painted &&
this.clear_background_color
) {
ctx.fillStyle = this.clear_background_color
ctx.fillRect(
this.visible_area[0],
this.visible_area[1],
this.visible_area[2],
this.visible_area[3]
)
}
// 主要修改
if (this.background_image && this.ds.scale > 0.5 && !bg_already_painted) {
if (this.zoom_modify_alpha) {
//使得 alpha 越接近0时变化越缓慢。
let alpha = (1.0 - 0.5 / this.ds.scale) * this.editor_alpha
ctx.globalAlpha = Math.min(Math.max(0, Math.sqrt(alpha)), 1)
// console.log((1.0 - 0.5 / this.ds.scale) * this.editor_alpha)
} else {
ctx.globalAlpha = this.editor_alpha
}
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false // ctx.mozImageSmoothingEnabled =
if (!this._bg_img || this._bg_img.name != this.background_image) {
this._bg_img = new Image()
this._bg_img.name = this.background_image
this._bg_img.src = this.background_image
var that = this
this._bg_img.onload = function () {
that.draw(true, true)
}
}
var pattern = null
if (this._pattern == null && this._bg_img.width > 0) {
pattern = ctx.createPattern(this._bg_img, 'repeat')
this._pattern_img = this._bg_img
this._pattern = pattern
} else {
pattern = this._pattern
}
if (pattern) {
ctx.fillStyle = pattern
ctx.fillRect(
this.visible_area[0],
this.visible_area[1],
this.visible_area[2],
this.visible_area[3]
)
ctx.fillStyle = 'transparent'
}
ctx.globalAlpha = 1.0
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true //= ctx.mozImageSmoothingEnabled
}
//groups
if (this.graph._groups.length && !this.live_mode) {
this.drawGroups(canvas, ctx)
}
if (this.onDrawBackground) {
this.onDrawBackground(ctx, this.visible_area)
}
if (this.onBackgroundRender) {
//LEGACY
console.error(
'WARNING! onBackgroundRender deprecated, now is named onDrawBackground '
)
this.onBackgroundRender = null
}
//DEBUG: show clipping area
//ctx.fillStyle = "red";
//ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20);
//bg
if (this.render_canvas_border) {
ctx.strokeStyle = '#235'
ctx.strokeRect(0, 0, canvas.width, canvas.height)
}
if (this.render_connections_shadows) {
ctx.shadowColor = '#000'
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 0
ctx.shadowBlur = 6
} else {
ctx.shadowColor = 'rgba(0,0,0,0)'
}
//draw connections
if (!this.live_mode) {
this.drawConnections(ctx)
}
ctx.shadowColor = 'rgba(0,0,0,0)'
//restore state
ctx.restore()
}
if (ctx.finish) {
ctx.finish()
}
this.dirty_bgcanvas = false
this.dirty_canvas = true //to force to repaint the front canvas with the bgcanvas
}
function imgToCanvasBase64 (img) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
const base64 = canvas.toDataURL('image/png')
return base64
}
// 使用示例
function convertImageToBase64 (img) {
// const img = new Image()
// img.src = 'path/to/your/image.jpg' // 替换为你的图片路径
// console.log('convertImageToBase64',img)
try {
const base64 = imgToCanvasBase64(img)
return base64
} catch (error) {
console.error(error)
}
}
function getInputsAndOutputs () {
const outputs =
`PreviewImage,SaveImage,TransparentImage,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_`.split(
','
)
let outputsId = []
for (let node of app.graph._nodes) {
if (outputs.includes(node.type)) {
outputsId.push(node.id)
}
}
return outputsId
}
function getRandomElement (arr) {
const randomIndex = Math.floor(Math.random() * arr.length)
return arr[randomIndex]
}
async function getBG () {
var outputs = []
for (let id of app.graph
.getNodeById(50)
.widgets.filter(w => w.name === 'output_ids')[0]
.value.split('\n')) {
if (getInputsAndOutputs().map(Number).includes(Number(id))) {
if (app.graph.getNodeById(id).imgs && app.graph.getNodeById(id).imgs[0]) {
let b = convertImageToBase64(app.graph.getNodeById(id).imgs[0])
// console.log(b)
outputs.push(b)
}
}
}
var BACKGROUND_IMAGE = getRandomElement(outputs),
CLEAR_BACKGROUND_COLOR = 'rgba(0,0,0,0.9)'
if (!window._bg_img) {
window._bg_img = app.canvas._bg_img.src
}
// let img=new Image();
// img.src=BACKGROUND_IMAGE;
//去掉透明度过度
// app.canvas.zoom_modify_alpha=false;
//整体透明度
app.canvas.editor_alpha = 1.1
// app.canvas._pattern=ctx.createPattern(img, "no-repeat");
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR)
app.canvas.draw(true, true)
}
class BgRunner {
constructor () {
this.intervalId = null
this.running = false
}
// 要运行的方法
bg () {
console.log('方法bg正在运行')
getBG()
}
// 启动bg方法每秒运行一次
start () {
if (!this.running) {
this.intervalId = setInterval(() => this.bg(), 1500)
this.running = true
}
}
// 停止bg方法的运行
stop () {
if (this.running) {
clearInterval(this.intervalId)
this.intervalId = null
this.running = false
if (window._bg_img) {
var BACKGROUND_IMAGE = window._bg_img,
CLEAR_BACKGROUND_COLOR = 'rgba(0,0,0,1)'
app.canvas.editor_alpha = 1
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR)
app.canvas.draw(true, true)
}
}
}
// 切换start和stop
toggle () {
if (this.running) {
this.stop()
} else {
this.start()
}
}
// 获取运行状态
isRunning () {
return this.running
}
}
// 示例用法
// const runner = new BgRunner();
// runner.start();
// setTimeout(() => runner.stop(), 5000);
export const td_bg = new BgRunner()
File diff suppressed because it is too large Load Diff
+163 -67
View File
@@ -1,46 +1,13 @@
import { app } from '../../../scripts/app.js'
import { $el } from '../../../scripts/ui.js'
import { $el } from '../../../scripts/ui.js'
import {
loadExternalScript,
updateLLMAPIKey,
get_position_style,
getLocalData
} from './common.js'
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
loadExternalScript('/mixlab/app/lib/pickr.min.js')
function hexToRGBA (hexColor) {
var hex = hexColor.replace('#', '')
@@ -62,7 +29,7 @@ app.registerExtension({
init () {
$el('link', {
rel: 'stylesheet',
href: '/extensions/comfyui-mixlab-nodes/lib/classic.min.css',
href: '/mixlab/app/lib/classic.min.css',
parent: document.head
})
@@ -122,7 +89,7 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
// console.log('Color nodeData', this.widgets)
// console.log('Color nodeData', this.div)
const widget = {
type: 'div',
@@ -273,19 +240,19 @@ app.registerExtension({
})
const min_max = node => {
if(node.widgets){
if (node.widgets) {
const min_value = node.widgets.filter(w => w.name === 'min_value')[0]
const max_value = node.widgets.filter(w => w.name === 'max_value')[0]
const number = node.widgets.filter(w => w.name === 'number')[0]
if (number) {
number.options.min = min_value.value
number.options.max = max_value.value
number.value = Math.min(number.options.max, number.value)
number.value = Math.max(number.options.min, number.value)
}
if (min_value)
min_value.callback = e => {
number.options.min = e
@@ -297,22 +264,18 @@ const min_max = node => {
number.value = e
}
}
}
app.registerExtension({
name: 'Mixlab.utils.FloatSlider',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'FloatSlider') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated;
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
min_max(this)
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'FloatSlider') {
@@ -323,7 +286,6 @@ app.registerExtension({
app.registerExtension({
name: 'Mixlab.utils.IntNumber',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'IntNumber') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
@@ -331,7 +293,6 @@ app.registerExtension({
min_max(this)
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'IntNumber') {
@@ -340,22 +301,157 @@ app.registerExtension({
}
})
app.registerExtension({
name: 'Mixlab.utils.TESTNODE_',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'TESTNODE_') {
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
console.log('##',message)
};
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
console.log('##', message)
}
}
},
}
})
app.registerExtension({
name: 'Mixlab.utils.KeyInput',
init () {},
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_llm_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'KeyInput') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const rowHeight = this.rowHeight
const widget = {
type: 'div',
name: 'input_key',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 24, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
div.style = `
display: flex;
align-items: center;
margin: 6px 8px;
margin-top:0px;
height:44px;
width:220px;
`
const ip = document.createElement('input')
ip.type = 'password'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
ip.placeholder = placeholder
// ip.value = placeholder
ip.style = `margin-left:8px;
outline: none;
border: none;
padding:12px;
width: 100%;
`
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
updateLLMAPIKey(data[this.id])
})
return div
}
let inputKey = inputDiv('_mixlab_llm_api_key', 'Key')
widget.div.appendChild(inputKey)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
// const processMouseWheel=app.canvas.processMouseWheel
// app.canvas.processMouseWheel=()=>{
// console.log(app.canvas.ds.scale)
// return processMouseWheel?.()
// }
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'KeyInput') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_llm_api_key')
let id = node.id
if (widget.div.querySelector('.Key'))
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
if (apiKey[id]) updateLLMAPIKey(apiKey[id])
}
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
// console.log('#LoadImagesToBatch', node.type)
if (node.type === 'KeyInput') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_llm_api_key')
let id = node.id
if (widget.div.querySelector('.Key'))
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
if (apiKey[id]) updateLLMAPIKey(apiKey[id])
}
}, 1000)
}
})
+49 -50
View File
@@ -6,8 +6,6 @@ import { $el } from '../../../scripts/ui.js'
// The code is based on ComfyUI-VideoHelperSuite modification.
function injectCSS (css) {
// 检查页面中是否已经存在具有相同内容的style标签
const existingStyle = document.querySelector('style')
@@ -45,7 +43,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -240,15 +241,7 @@ app.registerExtension({
}
})
function offsetDOMWidget(
widget,
ctx,
node,
widgetWidth,
widgetY,
height
) {
function offsetDOMWidget (widget, ctx, node, widgetWidth, widgetY, height) {
const margin = 10
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
@@ -270,18 +263,18 @@ function offsetDOMWidget(
position: 'absolute',
background: !node.color ? '' : node.color,
color: !node.color ? '' : 'white',
zIndex: 5, //app.graph._nodes.indexOf(node),
zIndex: 5 //app.graph._nodes.indexOf(node),
})
}
export const hasWidgets = (node) => {
export const hasWidgets = node => {
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
return false
}
return true
}
export const cleanupNode = (node) => {
export const cleanupNode = node => {
if (!hasWidgets(node)) {
return
}
@@ -298,43 +291,43 @@ export const cleanupNode = (node) => {
}
}
const CreatePreviewElement = (name, val, format) => {
const [type] = format.split('/')
const createPreviewElement = (name, val, format) => {
const [type] = format.split('/')
const w = {
name,
type,
value: val,
draw: function (ctx, node, widgetWidth, widgetY, height) {
const [cw, ch] = this.computeSize(widgetWidth)
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
},
computeSize: function (_) {
const ratio = this.inputRatio || 1
const width = Math.max(220, this.parent.size[0])
return [width, (width / ratio + 10)]
},
onRemoved: function () {
if (this.inputEl) {
this.inputEl.remove()
}
},
name,
type,
value: val,
draw: function (ctx, node, widgetWidth, widgetY, height) {
const [cw, ch] = this.computeSize(widgetWidth)
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
},
computeSize: function (_) {
const ratio = this.inputRatio || 1
const width = Math.max(220, this.parent.size[0])
return [width, width / ratio + 10]
},
onRemoved: function () {
if (this.inputEl) {
this.inputEl.remove()
}
}
w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
w.inputEl.src = w.value
if (type === 'video') {
w.inputEl.setAttribute('type', 'video/webm');
w.inputEl.autoplay = true
w.inputEl.loop = true
w.inputEl.controls = false;
}
w.inputEl.onload = function () {
w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
}
document.body.appendChild(w.inputEl)
return w
}
w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
w.inputEl.src = w.value
if (type === 'video' || format.match('.mp4')) {
w.inputEl.setAttribute('type', 'video/webm')
w.inputEl.autoplay = true
w.inputEl.loop = true
w.inputEl.controls = true
}
w.inputEl.onload = function () {
w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
}
document.body.appendChild(w.inputEl)
return w
}
app.registerExtension({
name: 'Mixlab.Video.ImageListReplace',
@@ -469,12 +462,17 @@ app.registerExtension({
}
}
if (nodeData?.name == 'VideoCombine_Adv') {
if (
nodeData?.name == 'VideoCombine_Adv' ||
nodeData?.name == 'CombineAudioVideo'
) {
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
const prefix = 'vhs_gif_preview_'
const r = onExecuted ? onExecuted.apply(this, message) : undefined
if(!this.widgets) this.widgets=[]
if (this.widgets) {
const pos = this.widgets.findIndex(w => w.name === `${prefix}_0`)
if (pos !== -1) {
@@ -489,12 +487,13 @@ app.registerExtension({
'/view?' + new URLSearchParams(params).toString()
)
const w = this.addCustomWidget(
CreatePreviewElement(
createPreviewElement(
`${prefix}_${i}`,
previewUrl,
params.format || 'image/gif'
)
)
console.log(w)
w.parent = this
})
}
+237
View File
@@ -0,0 +1,237 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
return data
}
// 上传得到url
async function uploadBase64ToFile (base64) {
let bg_blob = await base64ToBlobFromURL(base64)
let url = await uploadImage(bg_blob, '.png')
return url
}
class Visualizer {
constructor (node, container, visualSrc) {
this.node = node
this.iframe = document.createElement('iframe')
Object.assign(this.iframe, {
scrolling: 'no',
overflow: 'hidden'
})
this.iframe.src = '/mixlab/app/' + visualSrc + '.html'
console.log('#Visualizer', container, this.iframe)
container.appendChild(this.iframe)
}
updateVisual (params) {
console.log('#updateVisual', params, this.iframe)
// const iframeDocument = this.iframe.contentWindow.document
// const previewScript = iframeDocument.getElementById('visualizer')
// previewScript.setAttribute(
// 'reference_image',
// JSON.stringify(params.reference_image)
// )
// previewScript.setAttribute('depth_map', JSON.stringify(params.depth_map))
// Update the reference image and depth map
this.iframe.contentWindow.postMessage(params, '*')
}
remove () {
this.container.remove()
}
}
function createVisualizer (node, inputName, typeName, inputData, app) {
node.name = inputName
const widget = {
type: typeName,
name: 'preview3d',
callback: () => {},
draw: function (ctx, node, widgetWidth, widgetY, widgetHeight) {
const margin = 10
const top_offset = 5
const visible = app.canvas.ds.scale > 0.5 && this.type === typeName
const w = widgetWidth - margin * 4
const clientRectBound = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
clientRectBound.width / ctx.canvas.width,
clientRectBound.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + widgetY)
Object.assign(this.visualizer.style, {
left: `${transform.a * margin + transform.e}px`,
top: `${transform.d + transform.f + top_offset}px`,
width: `${w * transform.a}px`,
height: `${
w * transform.d - widgetHeight - margin * 15 * transform.d
}px`,
position: 'absolute',
overflow: 'hidden',
zIndex: app.graph._nodes.indexOf(node)
})
Object.assign(this.visualizer.children[0].style, {
transformOrigin: '50% 50%',
width: '100%',
height: '100%',
border: '0 none'
})
this.visualizer.hidden = !visible
}
}
const container = document.createElement('div')
container.id = `Comfy3D_${inputName}`
node.visualizer = new Visualizer(node, container, typeName)
widget.visualizer = container
widget.parent = node
document.body.appendChild(widget.visualizer)
node.addCustomWidget(widget)
node.updateParameters = params => {
// console.log('#updateParameters', params)
params.id = node.id
// node.visualizer = new Visualizer(node, container, typeName)
node.visualizer.updateVisual(params)
}
// Events for drawing backgound
node.onDrawBackground = function (ctx) {
if (!this.flags.collapsed) {
node.visualizer.iframe.hidden = false
} else {
node.visualizer.iframe.hidden = true
}
}
// Make sure visualization iframe is always inside the node when resize the node
node.onResize = function () {
let [w, h] = this.size
if (w <= 600) w = 600
if (h <= 500) h = 500
if (w > 600) {
h = w - 100
}
this.size = [w, h]
}
// Events for remove nodes
node.onRemoved = () => {
for (let w in node.widgets) {
if (node.widgets[w].visualizer) {
node.widgets[w].visualizer.remove()
}
}
}
return {
widget: widget
}
}
function registerVisualizer (nodeType, nodeData, nodeClassName, typeName) {
if (nodeData.name == nodeClassName) {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined
let Preview3DNode = app.graph._nodes.filter(
wi => wi.type == nodeClassName
)
let nodeName = `Preview3DNode_${Preview3DNode.length}`
const result = await createVisualizer.apply(this, [
this,
nodeName,
typeName,
{},
app
])
this.setSize([600, 500])
return r
}
nodeType.prototype.onExecuted = async function (message) {
// Check if reference image and depth map are available
if (message.reference_image && message.depth_map) {
const params = {}
params.reference_image = message.reference_image[0]
params.depth_map = message.depth_map[0]
this.updateParameters(params)
}
}
}
}
app.registerExtension({
name: 'Mixlab.nodes.depthviewer',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
registerVisualizer(nodeType, nodeData, 'DepthViewer', 'threeVisualizer')
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
let widget = node.widgets?.filter(w => w.name == 'preview3d')[0]
let framesWidget = node.widgets?.filter(w => w.name == 'frames')[0]
if (node.type === 'DepthViewer' && widget) {
let nodeId = node.id
//延迟才能获得this.id
widget.visualizer.querySelector('iframe').src += '?id=' + nodeId
// console.log('DepthViewer',widget)
window.addEventListener('message', async event => {
// 检查消息的来源,确保消息来自可信的源
console.log(event)
const { id, imgs } = event.data
if (id == nodeId) {
framesWidget.value = { images: [] }
for (const f of imgs) {
let file = await uploadBase64ToFile(f)
framesWidget.value.images.push(file)
}
// framesWidget.value.base64 = frames
framesWidget.value._seed = Math.random()
node.title = 'Input #' + imgs.length
}
})
}
}, 1000)
}
})
+5 -2
View File
@@ -34,7 +34,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -125,7 +128,7 @@ app.registerExtension({
window._mixlab_file_path_watcher = json.event_type
// widget.card.innerText = window._mixlab_file_path_watcher || ''
//运行
// document.querySelector('#queue-button').click()
if (app) app.queuePrompt()
}
})
}, 1000)
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixlab AR</title>
</head>
<body>
<script type="module">
import { api } from "/mixlab/app/javascript/api.js";
import Command from '/mixlab/app/javascript/command.js'
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+482
View File
@@ -0,0 +1,482 @@
class ComfyApi extends EventTarget {
#registered = new Set();
constructor() {
super();
this.api_host = location.host;
this.api_base = location.pathname.split('/').slice(0, -1).join('/');
this.initialClientId = sessionStorage.getItem("clientId");
}
apiURL(route) {
return this.api_base + route;
}
fetchApi(route, options) {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = {};
}
options.headers["Comfy-User"] = this.user;
return fetch(this.apiURL(route), options);
}
addEventListener(type, callback, options) {
super.addEventListener(type, callback, options);
this.#registered.add(type);
}
/**
* Poll status for colab and other things that don't support websockets.
*/
#pollQueue() {
setInterval(async () => {
try {
const resp = await this.fetchApi("/prompt");
const status = await resp.json();
this.dispatchEvent(new CustomEvent("status", { detail: status }));
} catch (error) {
this.dispatchEvent(new CustomEvent("status", { detail: null }));
}
}, 1000);
}
/**
* Creates and connects a WebSocket for realtime updates
* @param {boolean} isReconnect If the socket is connection is a reconnect attempt
*/
#createSocket(isReconnect) {
if (this.socket) {
return;
}
let opened = false;
let existingSession = window.name;
if (existingSession) {
existingSession = "?clientId=" + existingSession;
}
this.socket = new WebSocket(
`ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}`
);
this.socket.binaryType = "arraybuffer";
this.socket.addEventListener("open", () => {
opened = true;
if (isReconnect) {
this.dispatchEvent(new CustomEvent("reconnected"));
}
});
this.socket.addEventListener("error", () => {
if (this.socket) this.socket.close();
if (!isReconnect && !opened) {
this.#pollQueue();
}
});
this.socket.addEventListener("close", () => {
setTimeout(() => {
this.socket = null;
this.#createSocket(true);
}, 300);
if (opened) {
this.dispatchEvent(new CustomEvent("status", { detail: null }));
this.dispatchEvent(new CustomEvent("reconnecting"));
}
});
this.socket.addEventListener("message", (event) => {
try {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
const eventType = view.getUint32(0);
const buffer = event.data.slice(4);
switch (eventType) {
case 1:
const view2 = new DataView(event.data);
const imageType = view2.getUint32(0)
let imageMime
switch (imageType) {
case 1:
default:
imageMime = "image/jpeg";
break;
case 2:
imageMime = "image/png"
}
const imageBlob = new Blob([buffer.slice(4)], { type: imageMime });
this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob }));
break;
default:
throw new Error(`Unknown binary websocket message of type ${eventType}`);
}
}
else {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "status":
if (msg.data.sid) {
this.clientId = msg.data.sid;
window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
}
this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
break;
case "progress":
this.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
break;
case "executing":
this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.node }));
break;
case "executed":
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
break;
case "execution_start":
this.dispatchEvent(new CustomEvent("execution_start", { detail: msg.data }));
break;
case "execution_success":
this.dispatchEvent(new CustomEvent("execution_success", { detail: msg.data }));
break;
case "execution_error":
this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data }));
break;
case "execution_cached":
this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data }));
break;
default:
if (this.#registered.has(msg.type)) {
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
} else {
throw new Error(`Unknown message type ${msg.type}`);
}
}
}
} catch (error) {
console.warn("Unhandled message:", event.data, error);
}
});
}
/**
* Initialises sockets and realtime updates
*/
init() {
this.#createSocket();
}
/**
* Gets a list of extension urls
* @returns An array of script urls to import
*/
async getExtensions() {
const resp = await this.fetchApi("/extensions", { cache: "no-store" });
return await resp.json();
}
/**
* Gets a list of embedding names
* @returns An array of script urls to import
*/
async getEmbeddings() {
const resp = await this.fetchApi("/embeddings", { cache: "no-store" });
return await resp.json();
}
/**
* Loads node object definitions for the graph
* @returns The node definitions
*/
async getNodeDefs() {
const resp = await this.fetchApi("/object_info", { cache: "no-store" });
return await resp.json();
}
/**
*
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
* @param {object} prompt The prompt data to queue
*/
async queuePrompt(number, { output, workflow }) {
const body = {
client_id: this.clientId,
prompt: output,
extra_data: { extra_pnginfo: { workflow } },
};
if (number === -1) {
body.front = true;
} else if (number != 0) {
body.number = number;
}
const res = await this.fetchApi("/prompt", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status !== 200) {
throw {
response: await res.json(),
};
}
return await res.json();
}
/**
* Loads a list of items (queue or history)
* @param {string} type The type of items to load, queue or history
* @returns The items of the specified type grouped by their status
*/
async getItems(type) {
if (type === "queue") {
return this.getQueue();
}
return this.getHistory();
}
/**
* Gets the current state of the queue
* @returns The currently running and queued items
*/
async getQueue() {
try {
const res = await this.fetchApi("/queue");
const data = await res.json();
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.map((prompt) => ({
prompt,
remove: { name: "Cancel", cb: () => api.interrupt() },
})),
Pending: data.queue_pending.map((prompt) => ({ prompt })),
};
} catch (error) {
console.error(error);
return { Running: [], Pending: [] };
}
}
/**
* Gets the prompt execution history
* @returns Prompt history including node outputs
*/
async getHistory(max_items=200) {
try {
const res = await this.fetchApi(`/history?max_items=${max_items}`);
return { History: Object.values(await res.json()) };
} catch (error) {
console.error(error);
return { History: [] };
}
}
/**
* Gets system & device stats
* @returns System stats such as python version, OS, per device info
*/
async getSystemStats() {
const res = await this.fetchApi("/system_stats");
return await res.json();
}
/**
* Sends a POST request to the API
* @param {*} type The endpoint to post to
* @param {*} body Optional POST data
*/
async #postItem(type, body) {
try {
await this.fetchApi("/" + type, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
} catch (error) {
console.error(error);
}
}
/**
* Deletes an item from the specified list
* @param {string} type The type of item to delete, queue or history
* @param {number} id The id of the item to delete
*/
async deleteItem(type, id) {
await this.#postItem(type, { delete: [id] });
}
/**
* Clears the specified list
* @param {string} type The type of list to clear, queue or history
*/
async clearItems(type) {
await this.#postItem(type, { clear: true });
}
/**
* Interrupts the execution of the running prompt
*/
async interrupt() {
await this.#postItem("interrupt", null);
}
/**
* Gets user configuration data and where data should be stored
* @returns { Promise<{ storage: "server" | "browser", users?: Promise<string, unknown>, migrated?: boolean }> }
*/
async getUserConfig() {
return (await this.fetchApi("/users")).json();
}
/**
* Creates a new user
* @param { string } username
* @returns The fetch response
*/
createUser(username) {
return this.fetchApi("/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
});
}
/**
* Gets all setting values for the current user
* @returns { Promise<string, unknown> } A dictionary of id -> value
*/
async getSettings() {
return (await this.fetchApi("/settings")).json();
}
/**
* Gets a setting for the current user
* @param { string } id The id of the setting to fetch
* @returns { Promise<unknown> } The setting value
*/
async getSetting(id) {
return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json();
}
/**
* Stores a dictionary of settings for the current user
* @param { Record<string, unknown> } settings Dictionary of setting id -> value to save
* @returns { Promise<void> }
*/
async storeSettings(settings) {
return this.fetchApi(`/settings`, {
method: "POST",
body: JSON.stringify(settings)
});
}
/**
* Stores a setting for the current user
* @param { string } id The id of the setting to update
* @param { unknown } value The value of the setting
* @returns { Promise<void> }
*/
async storeSetting(id, value) {
return this.fetchApi(`/settings/${encodeURIComponent(id)}`, {
method: "POST",
body: JSON.stringify(value)
});
}
/**
* Gets a user data file for the current user
* @param { string } file The name of the userdata file to load
* @param { RequestInit } [options]
* @returns { Promise<Response> } The fetch response object
*/
async getUserData(file, options) {
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options);
}
/**
* Stores a user data file for the current user
* @param { string } file The name of the userdata file to save
* @param { unknown } data The data to save to the file
* @param { RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } } [options]
* @returns { Promise<Response> }
*/
async storeUserData(file, data, options = { overwrite: true, stringify: true, throwOnError: true }) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options?.overwrite}`, {
method: "POST",
body: options?.stringify ? JSON.stringify(data) : data,
...options,
});
if (resp.status !== 200 && options?.throwOnError !== false) {
throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`);
}
return resp;
}
/**
* Deletes a user data file for the current user
* @param { string } file The name of the userdata file to delete
*/
async deleteUserData(file) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
method: "DELETE",
});
if (resp.status !== 204) {
throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`);
}
}
/**
* Move a user data file for the current user
* @param { string } source The userdata file to move
* @param { string } dest The destination for the file
*/
async moveUserData(source, dest, options = { overwrite: false }) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, {
method: "POST",
});
return resp;
}
/**
* @overload
* Lists user data files for the current user
* @param { string } dir The directory in which to list files
* @param { boolean } [recurse] If the listing should be recursive
* @param { true } [split] If the paths should be split based on the os path separator
* @returns { Promise<string[][]>> } The list of split file paths in the format [fullPath, ...splitPath]
*/
/**
* @overload
* Lists user data files for the current user
* @param { string } dir The directory in which to list files
* @param { boolean } [recurse] If the listing should be recursive
* @param { false | undefined } [split] If the paths should be split based on the os path separator
* @returns { Promise<string[]>> } The list of files
*/
async listUserData(dir, recurse, split) {
const resp = await this.fetchApi(
`/userdata?${new URLSearchParams({
recurse,
dir,
split,
})}`
);
if (resp.status === 404) return [];
if (resp.status !== 200) {
throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`);
}
return resp.json();
}
}
export const api = new ComfyApi();
+697
View File
@@ -0,0 +1,697 @@
function get_url () {
// 如果有缓存记录
let hostUrl = localStorage.getItem('_hostUrl') || ''
if (hostUrl) {
return hostUrl
}
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
function getFilenameAndCategoryFromUrl (url) {
const queryString = url.split('?')[1]
if (!queryString) {
return {}
}
const params = new URLSearchParams(queryString)
const filename = params.get('filename')
? decodeURIComponent(params.get('filename'))
: null
const category = params.get('category')
? decodeURIComponent(params.get('category') || '')
: ''
return { category, filename }
}
async function get_my_app (category = '', filename = null) {
let url = get_url()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
mode: 'cors', // 允许跨域请求
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
task: 'my_app',
filename,
category
})
})
let result = await res.json()
let data = []
try {
for (const res of result.data) {
let { output, app } = res.data
if (app.filename)
data.push({
...app,
data: output,
date: res.date
})
}
} catch (error) {}
return data
}
async function getAppInit () {
const { category, filename } = getFilenameAndCategoryFromUrl(
window.location.href
)
return await get_my_app(category, filename)
}
function success (isSuccess, btn, text) {
isSuccess ? (btn.innerText = 'success') : text
setTimeout(() => {
btn.innerText = text
}, 5000)
}
async function interrupt () {
try {
await fetch(`${get_url()}/interrupt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: undefined
})
} catch (error) {
console.error(error)
}
return true
}
async function getQueue (clientId) {
try {
const res = await fetch(`${get_url()}/queue`)
const data = await res.json()
return {
// Running action uses a different endpoint for cancelling
Running: Array.from(data.queue_running, prompt => {
if (prompt[3].client_id === clientId) {
let prompt_id = prompt[1]
return {
prompt_id,
remove: () => interrupt()
}
}
}),
Pending: data.queue_pending.map(prompt => ({ prompt }))
}
} catch (error) {
console.error(error)
return { Running: [], Pending: [] }
}
}
// 请求历史数据
async function getPromptResult (category) {
let url = get_url()
try {
const response = await fetch(`${url}/mixlab/prompt_result`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'all'
})
})
if (response.ok) {
const data = await response.json()
console.log('#getPromptResult:', category, data)
return data.result.filter(r => r.appInfo.category == category)
// 处理返回的数据
} else {
console.log('Error:', response.status)
// 处理错误情况
}
} catch (error) {
console.log('Error:', error)
// 处理异常情况
}
}
// 新的运行工作流的接口
function queuePromptNew (
filename,
category,
seed,
input,
client_id,
apps = null
) {
let url = get_url()
// var filename = "Text-to-Image_1.json", category = "";
// 随机seed
// promptWorkflow = randomSeed(seed, promptWorkflow);
let d = { filename, category, seed, input, client_id }
if (apps) {
d.apps = apps
}
const data = JSON.stringify(d)
return new Promise((res, rej) => {
fetch(`${url}/mixlab/prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: data
})
.then(response => {
if (!response.ok) {
// Handle HTTP error responses
if (response.status === 400) {
return response.json().then(errorData => {
// Process the error data
console.error('Error 400:', errorData)
alert(JSON.stringify(errorData, null, 2))
res(null)
})
}
throw new Error('Network response was not ok')
}
return response.json() // Process the response data
})
.then(data => {
// Handle the response data
console.log('Success:', data)
res(true)
})
.catch(error => {
// Handle fetch errors
console.error('Fetch error:', error)
res(null)
})
})
}
// 保存历史数据
async function savePromptResult (data) {
let url = get_url()
try {
const response = await fetch(`${url}/mixlab/prompt_result`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'save',
data
})
})
if (response.ok) {
const res = await response.json()
console.log('Response:', res)
return res
// 处理返回的数据
} else {
console.log('Error:', response.status)
// 处理错误情况
}
} catch (error) {
console.log('Error:', error)
// 处理异常情况
}
}
async function uploadImage (blob, fileType = '.png', filename) {
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const url = get_url()
const resp = await fetch(`${url}/upload/image`, {
method: 'POST',
body
})
let data = await resp.json()
// console.log(data)
let { name, subfolder } = data
let src = `${url}/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}&rand=${Math.random()}`
return { url: src, name }
}
async function uploadMask (arrayBuffer, imgurl) {
const body = new FormData()
const filename = 'clipspace-mask-' + performance.now() + '.png'
let original_url = new URL(imgurl)
const original_ref = { filename: original_url.searchParams.get('filename') }
let original_subfolder = original_url.searchParams.get('subfolder')
if (original_subfolder) original_ref.subfolder = original_subfolder
let original_type = original_url.searchParams.get('type')
if (original_type) original_ref.type = original_type
body.append('image', arrayBuffer, filename)
body.append('original_ref', JSON.stringify(original_ref))
body.append('type', 'input')
body.append('subfolder', 'clipspace')
const url = get_url()
const resp = await fetch(`${url}/upload/mask`, {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
let { name, subfolder, type } = data
let src = `${url}/view?filename=${encodeURIComponent(
name
)}&type=${type}&subfolder=${subfolder}&rand=${Math.random()}`
return { url: src, name: 'clipspace/' + name }
}
const parseImageToBase64 = url => {
return new Promise((res, rej) => {
fetch(url)
.then(response => response.blob())
.then(blob => {
const reader = new FileReader()
reader.onloadend = () => {
const base64data = reader.result
res(base64data)
// 在这里可以将base64数据用于进一步处理或显示图片
}
reader.readAsDataURL(blob)
})
.catch(error => {
console.log('发生错误:', error)
})
})
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
function convertImageToBlackBasedOnAlpha (image) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// Draw the image onto the canvas
canvas.width = image.width
canvas.height = image.height
ctx.drawImage(image, 0, 0)
// Get the image data from the canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const pixels = imageData.data
// Modify the RGB values based on the alpha channel
for (let i = 0; i < pixels.length; i += 4) {
const alpha = pixels[i + 3]
if (alpha !== 0) {
// Set non-transparent pixels to black
// 蒙版是黑色?
pixels[i] = 0 // Red
pixels[i + 1] = 255 // Green
pixels[i + 2] = 0 // Blue
}
}
// Put the modified image data back onto the canvas
ctx.putImageData(imageData, 0, 0)
// Convert the modified canvas to base64 data URL
const base64ImageData = canvas.toDataURL('image/png') // Replace 'png' with your desired image format
return base64ImageData
}
const blobToBase64 = blob => {
return new Promise((res, rej) => {
const reader = new FileReader()
reader.onloadend = () => {
const base64data = reader.result
res(base64data)
// 在这里可以将base64数据用于进一步处理或显示图片
}
reader.readAsDataURL(blob)
})
}
function base64ToBlob (base64) {
// 去除base64编码中的前缀
const base64WithoutPrefix = base64.replace(/^data:image\/\w+;base64,/, '')
// 将base64编码转换为字节数组
const byteCharacters = atob(base64WithoutPrefix)
// 创建一个存储字节数组的数组
const byteArrays = []
// 将字节数组放入数组中
for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
const slice = byteCharacters.slice(offset, offset + 1024)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
// 创建blob对象
const blob = new Blob(byteArrays, { type: 'image/png' }) // 根据实际情况设置MIME类型
return blob
}
async function calculateImageHash (blob) {
const buffer = await blob.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map(byte => byte.toString(16).padStart(2, '0'))
.join('')
return hashHex
}
// 获取 rembg 模型
async function get_rembg_models () {
try {
const response = await fetch(`${get_url()}/mixlab/folder_paths`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'rembg'
})
})
const data = await response.json()
// console.log(data)
return data.names
} catch (error) {
console.error(error)
}
}
//自动抠图
async function run_rembg (model, base64) {
try {
const response = await fetch(`${get_url()}/mixlab/rembg`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model,
base64
})
})
const data = await response.json()
// console.log(data)
return data.data
} catch (error) {
console.error(error)
}
}
function copyHtmlWithImagesToClipboard (data, cb) {
// 创建一个临时div元素
const tempDiv = document.createElement('div')
// 将HTML字符串赋值给div的innerHTML属性
tempDiv.innerHTML = data
// 获取div中的所有图像元素
const images = tempDiv.getElementsByTagName('img')
// 遍历图像元素,并将图像数据转换为Base64编码
for (let i = 0; i < images.length; i++) {
const image = images[i]
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
// 设置canvas尺寸与图像尺寸相同
canvas.width = image.width
canvas.height = image.height
// 在canvas上绘制图像
context.drawImage(image, 0, 0)
// 将canvas转换为Base64编码
const imageData = canvas.toDataURL()
// 将Base64编码替换图像元素的src属性
image.src = imageData
}
let richText = tempDiv.innerHTML
// 创建一个新的Blob对象,并将富文本字符串作为数据传递进去
const blob = new Blob([richText], { type: 'text/html' })
// 创建一个ClipboardItem对象,并将Blob对象添加到其中
const clipboardItem = new ClipboardItem({ 'text/html': blob })
// 使用Clipboard API将内容复制到剪贴板
navigator.clipboard
.write([clipboardItem])
.then(() => {
console.log('富文本已成功复制到剪贴板')
tempDiv.remove()
if (cb) cb(true)
})
.catch(error => {
console.error('复制到剪贴板失败:', error)
tempDiv.remove()
if (cb) cb(false)
})
}
function copyImagesToClipboard (html, cb) {
const tempDiv = document.createElement('div')
tempDiv.innerHTML = html
const images = tempDiv.querySelectorAll('img')
const promises = Array.from(images).map(image => {
return new Promise(resolve => {
const img = new Image()
img.src = image.src
img.onload = () => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
context.drawImage(img, 0, 0)
canvas.toBlob(blob => {
const clipboardItem = new ClipboardItem({ 'image/png': blob })
navigator.clipboard
.write([clipboardItem])
.then(() => {
resolve()
tempDiv.remove()
if (cb) cb(true)
})
.catch(error => {
reject(error)
tempDiv.remove()
if (cb) cb(false)
})
})
}
})
})
Promise.all([...promises])
.then(() => {
console.log('所有图片已成功复制到剪贴板')
if (cb) cb(true)
tempDiv.remove()
})
.catch(error => {
console.error('复制到剪贴板失败:', error)
if (cb) cb(false)
tempDiv.remove()
})
}
function copyTextToClipboard (html, cb) {
const tempDiv = document.createElement('div')
tempDiv.innerHTML = html
const text = tempDiv.innerText
const textData = new ClipboardItem({
'text/plain': new Blob([text], { type: 'text/plain' })
})
navigator.clipboard
.write([textData])
.then(() => {
console.log('所有文本已成功复制到剪贴板', text)
if (cb) cb(true)
tempDiv.remove()
})
.catch(error => {
console.error('复制到剪贴板失败:', error)
if (cb) cb(false)
tempDiv.remove()
})
}
// ComfyUI\web\extensions\core\dynamicPrompts.js
// 官方实现修改
// Allows for simple dynamic prompt replacement
// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
/*
* Strips C-style line and block comments from a string
*/
function dynamicPrompts (prompt) {
prompt = prompt.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
while (
prompt.replace('\\{', '').includes('{') &&
prompt.replace('\\}', '').includes('}')
) {
const startIndex = prompt.replace('\\{', '00').indexOf('{')
const endIndex = prompt.replace('\\}', '00').indexOf('}')
const optionsString = prompt.substring(startIndex + 1, endIndex)
const options = optionsString.split('|')
const randomIndex = Math.floor(Math.random() * options.length)
const randomOption = options[randomIndex]
prompt =
prompt.substring(0, startIndex) +
randomOption +
prompt.substring(endIndex + 1)
}
return prompt
}
// 遍历所有组合,语法同 动态提示
function generateAllCombinations (prompt) {
prompt = prompt.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
// Helper function to get all combinations
function getAllCombinations (parts) {
if (parts.length === 0) return ['']
const [firstPart, ...restParts] = parts
const restCombinations = getAllCombinations(restParts)
const allCombinations = []
firstPart.forEach(option => {
restCombinations.forEach(combination => {
allCombinations.push(option + combination)
})
})
return allCombinations
}
// Split prompt into static parts and dynamic parts
let parts = []
let startIndex = 0
while (
prompt.replace('\\{', '').includes('{') &&
prompt.replace('\\}', '').includes('}')
) {
startIndex = prompt.replace('\\{', '00').indexOf('{')
const endIndex = prompt.replace('\\}', '00').indexOf('}')
const staticPart = prompt.substring(0, startIndex)
const optionsString = prompt.substring(startIndex + 1, endIndex)
const options = optionsString.split('|')
parts.push([staticPart])
parts.push(options)
prompt = prompt.substring(endIndex + 1)
}
// Add the remaining static part
parts.push([prompt])
// Get all combinations
const combinations = getAllCombinations(parts)
return combinations
}
const _textNodes = [
'TextInput_',
'CLIPTextEncode',
'PromptSimplification',
'ChinesePrompt_Mix'
],
_loraNodes = ['CheckpointLoaderSimple', 'LoraLoader'],
_numberNodes = ['FloatSlider', 'IntNumber'],
_slideNodes = ['PromptSlide'],
_imageNodes = [
'LoadImage',
'VHS_LoadVideo',
'ImagesPrompt_',
'LoadImagesToBatch'
],
_colorNodes = ['Color'],
_audioNodes = ['LoadAndCombinedAudio_']
export default {
get_url,
get_my_app,
getAppInit,
getFilenameAndCategoryFromUrl,
success,
interrupt,
getQueue,
queuePromptNew,
savePromptResult,
uploadImage,
uploadMask,
run_rembg,
get_rembg_models,
parseImageToBase64,
createImage,
convertImageToBlackBasedOnAlpha,
blobToBase64,
base64ToBlob,
calculateImageHash,
copyHtmlWithImagesToClipboard,
copyImagesToClipboard,
copyTextToClipboard,
dynamicPrompts,
generateAllCombinations,
_textNodes,
_loraNodes,
_numberNodes,
_slideNodes,
_imageNodes,
_colorNodes,
_audioNodes
}
+347
View File
@@ -0,0 +1,347 @@
/* juxtapose - v1.2.2 - 2020-09-03
* Copyright (c) 2020 Alex Duner and Northwestern University Knight Lab
*/
div.juxtapose {
width: 100%;
font-family: Helvetica, Arial, sans-serif;
}
div.jx-slider {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
cursor: pointer;
color: #f3f3f3;
}
div.jx-handle {
position: absolute;
height: 100%;
width: 40px;
cursor: col-resize;
z-index: 15;
margin-left: -20px;
}
.vertical div.jx-handle {
height: 40px;
width: 100%;
cursor: row-resize;
margin-top: -20px;
margin-left: 0;
}
div.jx-control {
height: 100%;
margin-right: auto;
margin-left: auto;
width: 3px;
background-color: currentColor;
}
.vertical div.jx-control {
height: 3px;
width: 100%;
background-color: currentColor;
position: relative;
top: 50%;
transform: translateY(-50%);
}
div.jx-controller {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
height: 60px;
width: 9px;
margin-left: -3px;
background-color: currentColor;
}
.vertical div.jx-controller {
height: 9px;
width: 100px;
margin-left: auto;
margin-right: auto;
top: -3px;
position: relative;
}
div.jx-arrow {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
width: 0;
height: 0;
transition: all .2s ease;
}
.vertical div.jx-arrow {
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
width: 0;
height: 0;
transition: all .2s ease;
}
div.jx-arrow.jx-left {
left: 2px;
border-style: solid;
border-width: 8px 8px 8px 0;
border-color: transparent currentColor transparent transparent;
}
div.jx-arrow.jx-right {
right: 2px;
border-style: solid;
border-width: 8px 0 8px 8px;
border-color: transparent transparent transparent currentColor;
}
.vertical div.jx-arrow.jx-left {
left: 0px;
top: 2px;
border-style: solid;
border-width: 0px 8px 8px 8px;
border-color: transparent transparent currentColor transparent;
}
.vertical div.jx-arrow.jx-right {
right: 0px;
top: auto;
bottom: 2px;
border-style: solid;
border-width: 8px 8px 0 8px;
border-color: currentColor transparent transparent transparent;
}
div.jx-handle:hover div.jx-arrow.jx-left,
div.jx-handle:active div.jx-arrow.jx-left {
left: -1px;
}
div.jx-handle:hover div.jx-arrow.jx-right,
div.jx-handle:active div.jx-arrow.jx-right {
right: -1px;
}
.vertical div.jx-handle:hover div.jx-arrow.jx-left,
.vertical div.jx-handle:active div.jx-arrow.jx-left {
left: 0px;
top: 0px;
}
.vertical div.jx-handle:hover div.jx-arrow.jx-right,
.vertical div.jx-handle:active div.jx-arrow.jx-right {
right: 0px;
bottom: 0px;
}
div.jx-image {
position: absolute;
height: 100%;
display: inline-block;
top: 0;
overflow: hidden;
-webkit-backface-visibility: hidden;
}
.vertical div.jx-image {
width: 100%;
left: 0;
top: auto;
}
div.jx-image img {
height: 100%;
width: auto;
z-index: 5;
position: absolute;
margin-bottom: 0;
max-height: none;
max-width: none;
max-height: initial;
max-width: initial;
}
.vertical div.jx-image img {
height: auto;
width: 100%;
}
div.jx-image.jx-left {
left: 0;
background-position: left;
}
div.jx-image.jx-left img {
left: 0;
}
div.jx-image.jx-right {
right: 0;
background-position: right;
}
div.jx-image.jx-right img {
right: 0;
bottom: 0;
}
.veritcal div.jx-image.jx-left {
top: 0;
background-position: top;
}
.veritcal div.jx-image.jx-left img {
top: 0;
}
.vertical div.jx-image.jx-right {
bottom: 0;
background-position: bottom;
}
.veritcal div.jx-image.jx-right img {
bottom: 0;
}
div.jx-image div.jx-label {
font-size: 1em;
padding: .25em .75em;
position: relative;
display: inline-block;
top: 0;
background-color: #000; /* IE 8 */
background-color: rgba(0,0,0,.7);
color: white;
z-index: 10;
white-space: nowrap;
line-height: 18px;
vertical-align: middle;
}
div.jx-image.jx-left div.jx-label {
float: left;
left: 0;
}
div.jx-image.jx-right div.jx-label {
float: right;
right: 0;
}
.vertical div.jx-image div.jx-label {
display: table;
position: absolute;
}
.vertical div.jx-image.jx-right div.jx-label {
left: 0;
bottom: 0;
top: auto;
}
div.jx-credit {
line-height: 1.1;
font-size: 0.75em;
}
div.jx-credit em {
font-weight: bold;
font-style: normal;
}
/* Animation */
div.jx-image.transition {
transition: width .5s ease;
}
div.jx-handle.transition {
transition: left .5s ease;
}
.vertical div.jx-image.transition {
transition: height .5s ease;
}
.vertical div.jx-handle.transition {
transition: top .5s ease;
}
/* Knight Lab Credit */
a.jx-knightlab {
background-color: #000; /* IE 8 */
background-color: rgba(0,0,0,.25);
bottom: 0;
display: table;
height: 14px;
line-height: 14px;
padding: 1px 4px 1px 5px;
position: absolute;
right: 0;
text-decoration: none;
z-index: 10;
}
a.jx-knightlab div.knightlab-logo {
display: inline-block;
vertical-align: middle;
height: 8px;
width: 8px;
background-color: #c34528;
transform: rotate(45deg);
-ms-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
top: -1.25px;
position: relative;
cursor: pointer;
}
a.jx-knightlab:hover {
background-color: #000; /* IE 8 */
background-color: rgba(0,0,0,.35);
}
a.jx-knightlab:hover div.knightlab-logo {
background-color: #ce4d28;
}
a.jx-knightlab span.juxtapose-name {
display: table-cell;
margin: 0;
padding: 0;
font-family: Helvetica, Arial, sans-serif;
font-weight: 300;
color: white;
font-size: 10px;
padding-left: 0.375em;
vertical-align: middle;
line-height: normal;
text-shadow: none;
}
/* keyboard accessibility */
div.jx-controller:focus,
div.jx-image.jx-left div.jx-label:focus,
div.jx-image.jx-right div.jx-label:focus,
a.jx-knightlab:focus {
background: #eae34a;
color: #000;
}
a.jx-knightlab:focus span.juxtapose-name{
color: #000;
border: none;
}
+8
View File
File diff suppressed because one or more lines are too long
View File
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
/**
* https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts
*/
import {
BackSide,
BoxGeometry,
Mesh,
MeshBasicMaterial,
MeshStandardMaterial,
PointLight,
Scene,
} from './three.module.js';
class RoomEnvironment extends Scene {
constructor( renderer = null ) {
super();
const geometry = new BoxGeometry();
geometry.deleteAttribute( 'uv' );
const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
const boxMaterial = new MeshStandardMaterial();
const mainLight = new PointLight( 0xffffff, 900, 28, 2 );
mainLight.position.set( 0.418, 16.199, 0.300 );
this.add( mainLight );
const room = new Mesh( geometry, roomMaterial );
room.position.set( - 0.757, 13.219, 0.717 );
room.scale.set( 31.713, 28.305, 28.591 );
this.add( room );
const box1 = new Mesh( geometry, boxMaterial );
box1.position.set( - 10.906, 2.009, 1.846 );
box1.rotation.set( 0, - 0.195, 0 );
box1.scale.set( 2.328, 7.905, 4.651 );
this.add( box1 );
const box2 = new Mesh( geometry, boxMaterial );
box2.position.set( - 5.607, - 0.754, - 0.758 );
box2.rotation.set( 0, 0.994, 0 );
box2.scale.set( 1.970, 1.534, 3.955 );
this.add( box2 );
const box3 = new Mesh( geometry, boxMaterial );
box3.position.set( 6.167, 0.857, 7.803 );
box3.rotation.set( 0, 0.561, 0 );
box3.scale.set( 3.927, 6.285, 3.687 );
this.add( box3 );
const box4 = new Mesh( geometry, boxMaterial );
box4.position.set( - 2.017, 0.018, 6.124 );
box4.rotation.set( 0, 0.333, 0 );
box4.scale.set( 2.002, 4.566, 2.064 );
this.add( box4 );
const box5 = new Mesh( geometry, boxMaterial );
box5.position.set( 2.291, - 0.756, - 2.621 );
box5.rotation.set( 0, - 0.286, 0 );
box5.scale.set( 1.546, 1.552, 1.496 );
this.add( box5 );
const box6 = new Mesh( geometry, boxMaterial );
box6.position.set( - 2.193, - 0.369, - 5.547 );
box6.rotation.set( 0, 0.516, 0 );
box6.scale.set( 3.875, 3.487, 2.986 );
this.add( box6 );
// -x right
const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light1.position.set( - 16.116, 14.37, 8.208 );
light1.scale.set( 0.1, 2.428, 2.739 );
this.add( light1 );
// -x left
const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light2.position.set( - 16.109, 18.021, - 8.207 );
light2.scale.set( 0.1, 2.425, 2.751 );
this.add( light2 );
// +x
const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
light3.position.set( 14.904, 12.198, - 1.832 );
light3.scale.set( 0.15, 4.265, 6.331 );
this.add( light3 );
// +z
const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
light4.position.set( - 0.462, 8.89, 14.520 );
light4.scale.set( 4.38, 5.441, 0.088 );
this.add( light4 );
// -z
const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
light5.position.set( 3.235, 11.486, - 12.541 );
light5.scale.set( 2.5, 2.0, 0.1 );
this.add( light5 );
// +y
const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
light6.position.set( 0.0, 20.0, 0.0 );
light6.scale.set( 1.0, 0.1, 1.0 );
this.add( light6 );
}
dispose() {
const resources = new Set();
this.traverse( ( object ) => {
if ( object.isMesh ) {
resources.add( object.geometry );
resources.add( object.material );
}
} );
for ( const resource of resources ) {
resource.dispose();
}
}
}
function createAreaLightMaterial( intensity ) {
const material = new MeshBasicMaterial();
material.color.setScalar( intensity );
return material;
}
export { RoomEnvironment };
File diff suppressed because one or more lines are too long
+316
View File
@@ -0,0 +1,316 @@
import * as THREE from './three/three.module.js'
import { api } from '../../../scripts/api.js'
import { OrbitControls } from './three/OrbitControls.js'
import { RoomEnvironment } from './three/RoomEnvironment.js'
const visualizer = document.getElementById('visualizer')
const container = document.getElementById('container')
const progressDialog = document.getElementById('progress-dialog')
const progressIndicator = document.getElementById('progress-indicator')
const renderer = new THREE.WebGLRenderer({
antialias: true,
extensions: {
derivatives: true
}
})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
if (container) container.appendChild(renderer.domElement)
const pmremGenerator = new THREE.PMREMGenerator(renderer)
// scene
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
scene.environment = pmremGenerator.fromScene(
new RoomEnvironment(renderer),
0.04
).texture
const ambientLight = new THREE.AmbientLight(0xffffff)
const camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
0.1,
1000
)
camera.position.set(0, 0, 10)
const pointLight = new THREE.PointLight(0xffffff, 15)
camera.add(pointLight)
const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0, 0)
controls.update()
controls.enablePan = true
controls.enableDamping = true
// Handle window resize event
window.onresize = function () {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
var lastReferenceImage = ''
var lastDepthMap = ''
var needUpdate = false
function frameUpdate () {
var referenceImage = visualizer?.getAttribute('reference_image')
var depthMap = visualizer?.getAttribute('depth_map')
if (referenceImage == lastReferenceImage && depthMap == lastDepthMap) {
if (needUpdate) {
controls.update()
renderer.render(scene, camera)
}
requestAnimationFrame(frameUpdate)
} else {
needUpdate = false
scene.clear()
if (progressDialog) progressDialog.open = true
lastReferenceImage = referenceImage
lastDepthMap = depthMap
if (lastReferenceImage && lastReferenceImage != 'undefined') {
// console.log('lastReferenceImage',typeof(lastReferenceImage),lastDepthMap)
main(JSON.parse(lastReferenceImage), JSON.parse(lastDepthMap))
}
}
}
const onProgress = function (xhr) {
if (xhr.lengthComputable) {
progressIndicator.value = (xhr.loaded / xhr.total) * 100
}
}
const onError = function (e) {
console.error(e)
}
async function main (referenceImageParams, depthMapParams) {
let referenceTexture, depthTexture
let imageWidth = 10 // Default width
let imageHeight = 10 // Default height, will be updated based on the image's aspect ratio
// console.log('#referenceImageParams', referenceImageParams)
if (referenceImageParams?.filename) {
const referenceImageUrl = api
.apiURL('/view?' + new URLSearchParams(referenceImageParams))
.replace(/extensions.*\//, '')
const referenceImageExt = referenceImageParams.filename.slice(
referenceImageParams.filename.lastIndexOf('.') + 1
)
if (
referenceImageExt === 'png' ||
referenceImageExt === 'jpg' ||
referenceImageExt === 'jpeg'
) {
const referenceImageLoader = new THREE.TextureLoader()
referenceTexture = await new Promise((resolve, reject) => {
referenceImageLoader.load(
referenceImageUrl,
texture => {
// Once the image is loaded, update the width and height based on the image's aspect ratio
imageWidth = 10 // Keep the width as 10
imageHeight = texture.image.height / (texture.image.width / 10)
resolve(texture)
},
undefined,
reject
)
})
}
}
if (depthMapParams?.filename) {
const depthMapUrl = api
.apiURL('/view?' + new URLSearchParams(depthMapParams))
.replace(/extensions.*\//, '')
const depthMapExt = depthMapParams.filename.slice(
depthMapParams.filename.lastIndexOf('.') + 1
)
if (
depthMapExt === 'png' ||
depthMapExt === 'jpg' ||
depthMapExt === 'jpeg'
) {
const depthMapLoader = new THREE.TextureLoader()
depthTexture = await depthMapLoader.loadAsync(depthMapUrl)
}
}
if (referenceTexture && depthTexture) {
const depthMaterial = new THREE.ShaderMaterial({
uniforms: {
referenceTexture: { value: referenceTexture },
depthTexture: { value: depthTexture },
depthScale: { value: 5.0 },
ambientLightColor: { value: new THREE.Color(0.2, 0.2, 0.2) },
lightPosition: { value: new THREE.Vector3(2, 2, 2) },
lightColor: { value: new THREE.Color(1, 1, 1) },
lightIntensity: { value: 1.0 },
shininess: { value: 30 }
},
vertexShader: `
uniform sampler2D depthTexture;
uniform float depthScale;
varying vec2 vUv;
varying float vDepth;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vUv = uv;
float depth = texture2D(depthTexture, uv).r;
vec3 displacement = normal * depth * depthScale;
vec3 displacedPosition = position + displacement;
vec4 worldPosition = modelMatrix * vec4(displacedPosition, 1.0);
vNormal = normalize(normalMatrix * normal);
vViewPosition = (viewMatrix * worldPosition).xyz;
gl_Position = projectionMatrix * viewMatrix * worldPosition;
vDepth = depth;
}
`,
fragmentShader: `
uniform sampler2D referenceTexture;
varying vec2 vUv;
varying float vDepth;
void main() {
vec4 referenceColor = texture2D(referenceTexture, vUv);
// Directly use reference color without fog
gl_FragColor = referenceColor;
}
`
})
const planeGeometry = new THREE.PlaneGeometry(
imageWidth,
imageHeight,
200,
200
)
const depthMesh = new THREE.Mesh(planeGeometry, depthMaterial)
scene.add(depthMesh)
}
needUpdate = true
scene.add(ambientLight)
scene.add(camera)
progressDialog?.close()
frameUpdate()
}
document
.getElementById('screenshotButton')
?.addEventListener('click', takeScreenshot)
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
setTimeout(() => res(1), t)
})
}
// 方法:旋转摄像机并拍摄图片 // 每次旋转的角度增量,转换为弧度
async function captureImages (
totalFrames = 40,
angleIncrement = THREE.MathUtils.degToRad(0.5)
) {
// 计算场景中所有物体的中心点
const box = new THREE.Box3().setFromObject(scene)
const center = new THREE.Vector3()
box.getCenter(center)
// 计算当前相机距离中心点的半径
const radius = camera.position.distanceTo(center)
// 存储图片的数组
let images = []
// 记录初始相机位置和朝向
const initialPosition = camera.position.clone()
const initialTarget = center.clone()
// 计算当前相机的初始角度
const initialAngle = Math.atan2(
camera.position.z - center.z,
camera.position.x - center.x
)
// 起始角度为从当前角度往左旋转 20 度的位置
const startAngle = initialAngle - (angleIncrement * totalFrames) / 2
for (let i = 0; i < totalFrames; i++) {
const angle = startAngle + i * angleIncrement
// 计算相机的位置
camera.position.x = center.x + radius * Math.cos(angle)
camera.position.z = center.z + radius * Math.sin(angle)
camera.position.y = initialPosition.y // 保持相机高度不变
camera.lookAt(center) // 相机看向中心点
// 渲染当前帧
renderer.render(scene, camera)
// 将当前帧保存为图片
const imgData = renderer.domElement.toDataURL('image/png')
images.push(imgData)
// 等待一段时间
await new Promise(resolve => setTimeout(resolve, 500))
}
// 恢复相机到初始位置和朝向
camera.position.copy(initialPosition)
camera.lookAt(initialTarget)
return images
}
async function takeScreenshot () {
// 更新相机的矩阵,以确保其世界矩阵是最新的
camera.updateMatrixWorld()
const imgs = await captureImages()
// 获取当前网页的 URL
const currentUrl = window.location.href
// 创建一个 URL 对象
const url = new URL(currentUrl)
// 使用 URLSearchParams 获取参数
const params = new URLSearchParams(url.search)
// 获取参数 'id' 的值
const id = params.get('id')
window.parent.postMessage({ imgs, id }, '*')
}
main()
window.addEventListener('message', event => {
// 这里可以添加对来源的验证,以确保安全
// console.log('Message received from parent page:', event.data)
let { reference_image, depth_map } = event.data
if (reference_image && depth_map) {
visualizer?.setAttribute('reference_image', JSON.stringify(reference_image))
visualizer?.setAttribute('depth_map', JSON.stringify(depth_map))
frameUpdate()
}
})
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<meta charset="utf-8">
<!-- <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet"> -->
<title>p5.js-widget</title>
<div id="app-holder"></div>
<script src="./main.bundle.js"></script>
+240
View File
@@ -0,0 +1,240 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var defaults = __webpack_require__(1);
var MY_FILENAME = 'p5-widget.js';
var IFRAME_FILENAME = 'p5-widget.html';
var IFRAME_STYLE = [
'width: 100%',
'background-color: white',
'border: 1px solid #ec245e',
'box-sizing: border-box'
];
var AVOID_MIXED_CONTENT_WARNINGS = true;
var myScriptEl = getMyScriptEl();
var myBaseURL = getMyBaseURL(myScriptEl ? myScriptEl.src : "");
var autoload = myScriptEl ? !myScriptEl.hasAttribute('data-manual') : false;
var nextId = 1;
function getMyBaseURL(url) {
var baseURL = url.slice(0, -MY_FILENAME.length);
if (AVOID_MIXED_CONTENT_WARNINGS) {
if (window.location.protocol === 'http:' && /^https:/.test(baseURL)) {
// Our script was loaded over HTTPS, but the embedding page is
// using HTTP. This is likely to result in mixed content warnings
// if e.g. the widget's sketch wants to load resources relative to
// the embedding page's URL, so let's just embed the widget over
// HTTP instead of HTTPS.
baseURL = baseURL.replace('https:', 'http:');
}
}
return baseURL;
}
function getMyScriptEl() {
return (document.currentScript ||
document.querySelectorAll("script[src$='" + MY_FILENAME + "']")[0]);
}
// http://stackoverflow.com/a/7557433/2422398
function isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return (rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= (window.innerHeight ||
document.documentElement.clientHeight) &&
rect.left <= (window.innerWidth ||
document.documentElement.clientWidth));
}
function getDataHeight(el) {
var height = parseInt(el.getAttribute('data-height'));
if (isNaN(height))
height = defaults.HEIGHT;
return height;
}
function absoluteURL(url) {
var a = document.createElement('a');
a.setAttribute('href', url);
return a.href;
}
function getSketch(url, cb) {
var error = function (msg) {
var lines = ['// p5.js-widget failed to retrieve ' + url + '.'];
if (msg && typeof (msg) == 'string') {
lines.push('// ' + msg);
}
cb(lines.join('\n'));
};
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function () {
if (req.status == 200) {
cb(req.responseText);
}
else {
error('Server returned HTTP ' + req.status + '.');
}
};
req.onerror = error;
req.send(null);
}
function replaceScriptWithWidget(el) {
var iframe = document.createElement('iframe');
var height = getDataHeight(el);
var previewWidth = parseInt(el.getAttribute('data-preview-width'));
var baseSketchURL = absoluteURL(el.getAttribute('data-base-url'));
var p5version = el.getAttribute('data-p5-version');
var maxRunTime = parseInt(el.getAttribute('data-max-run-time'));
var autoplay = el.hasAttribute('data-autoplay');
var url;
var qsArgs = [
'id=' + encodeURIComponent(el.getAttribute('data-id'))
];
var style = IFRAME_STYLE.slice();
function makeWidget(sketch) {
qsArgs.push('sketch=' + encodeURIComponent(sketch));
style.push('min-height: ' + height + 'px');
url = myBaseURL + IFRAME_FILENAME + '?' + qsArgs.join('&');
iframe.setAttribute('src', url);
iframe.setAttribute('style', style.join('; '));
el.parentNode.replaceChild(iframe, el);
}
if (!isNaN(previewWidth) && previewWidth >= 0) {
qsArgs.push('previewWidth=' + previewWidth);
}
if (!isNaN(maxRunTime) && maxRunTime >= 0) {
qsArgs.push('maxRunTime=' + maxRunTime);
}
if (baseSketchURL) {
qsArgs.push('baseSketchURL=' + encodeURIComponent(baseSketchURL));
}
if (p5version) {
qsArgs.push('p5version=' + encodeURIComponent(p5version));
}
if (autoplay) {
qsArgs.push('autoplay=on');
}
if (el.src && el.textContent && el.textContent.trim()) {
return makeWidget([
'// Your widget includes both a "src" attribute and inline script',
'// content, which makes no sense. Please remove one of them.'
].join('\n'));
}
if (el.src) {
getSketch(el.src, makeWidget);
}
else {
makeWidget(el.textContent);
}
}
function whenVisible(el, cb) {
var CHECK_INTERVAL_MS = 1000;
var interval;
function maybeMakeVisible() {
if (!isElementInViewport(el))
return;
clearInterval(interval);
window.removeEventListener('scroll', maybeMakeVisible, false);
window.removeEventListener('resize', maybeMakeVisible, false);
cb(el);
}
// We want to check at a fixed interval as a fallback, to make
// sure that we detect when the element is visible even outside
// of the usual means (e.g., because the user did some
// sort of pinch/zoom gesture).
interval = setInterval(maybeMakeVisible, 1000);
window.addEventListener('scroll', maybeMakeVisible, false);
window.addEventListener('resize', maybeMakeVisible, false);
maybeMakeVisible();
}
function lazilyReplaceScriptWithWidget(el) {
var height = getDataHeight(el);
el.style.display = 'block';
el.style.fontSize = '0';
el.style.width = '100%';
el.style.minHeight = height + 'px';
el.style.background = '#f0f0f0';
if (!el.hasAttribute('data-id')) {
el.setAttribute('data-id', nextId.toString());
nextId++;
}
whenVisible(el, replaceScriptWithWidget);
}
function lazilyReplaceAllScriptsWithWidget() {
var scripts = document.querySelectorAll("script[type='text/p5']");
[].slice.call(scripts).forEach(function (el) {
lazilyReplaceScriptWithWidget(el);
});
}
if (autoload) {
if (document.readyState === 'complete') {
lazilyReplaceAllScriptsWithWidget();
}
else {
window.addEventListener('load', lazilyReplaceAllScriptsWithWidget, false);
}
}
window['p5Widget'] = {
baseURL: myBaseURL,
url: myBaseURL + MY_FILENAME,
replaceScript: lazilyReplaceScriptWithWidget,
replaceAll: lazilyReplaceAllScriptsWithWidget,
defaults: defaults
};
/***/ }),
/* 1 */
/***/ (function(module, exports) {
"use strict";
exports.P5_VERSION = '1.10.0';
exports.PREVIEW_WIDTH = 150;
exports.HEIGHT = 300;
exports.MAX_RUN_TIME = 1000;
/***/ })
/******/ ]);
//# sourceMappingURL=p5-widget.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,505 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ({
/***/ 0:
/***/ (function(module, exports, __webpack_require__) {
"use strict";
__webpack_require__(216);
// @ts-ignore
var global = window;
function loadScript(url, cb) {
var script = document.createElement('script');
cb = cb || (function () { });
script.onload = cb;
script.onerror = function () {
console.log('Failed to load script: ' + url);
};
script.setAttribute('src', url);
document.body.appendChild(script);
}
function loadScripts(urls, cb) {
cb = cb || (function () { });
var i = 0;
var loadNextScript = function () {
if (i === urls.length) {
return cb();
}
loadScript(urls[i++], loadNextScript);
};
loadNextScript();
}
function p5url(version) {
return "//cdnjs.cloudflare.com/ajax/libs/p5.js/" + version + "/p5.js";
}
function LoopChecker(sketch, funcName, maxRunTime) {
var self = {
wasTriggered: false,
getLineNumber: function () {
var index = loopCheckFailureRange[0];
var line = 1;
for (var i = 0; i < index; i++) {
if (sketch[i] === '\n')
line++;
}
return line;
}
};
var startTime = Date.now();
var loopCheckFailureRange = null;
global[funcName] = function (range) {
if (Date.now() - startTime > maxRunTime) {
self.wasTriggered = true;
loopCheckFailureRange = range;
throw new Error('Loop took over ' + maxRunTime + ' ms to run');
}
};
setInterval(function () {
startTime = Date.now();
}, maxRunTime / 2);
return self;
}
function setBaseURL(url) {
var base = document.createElement('base');
base.setAttribute('href', url);
document.head.appendChild(base);
}
function startSketch(sketch, p5version, maxRunTime, loopCheckFuncName, baseURL, errorCb) {
var sketchScript = document.createElement('script');
var loopChecker = LoopChecker(sketch, loopCheckFuncName, maxRunTime);
if (baseURL) {
setBaseURL(baseURL);
}
sketchScript.textContent = sketch;
global.addEventListener('error', function (e) {
var message = e.message;
var line = undefined;
// console.log(message)
if (loopChecker.wasTriggered) {
message = 'Your loop is taking too long to run.';
line = loopChecker.getLineNumber();
}
else if (typeof e.lineno === 'number' &&
(e.filename === '' || e.filename === window.location.href)) {
line = e.lineno;
}
// p5 sketches don't actually stop looping if they throw an exception,
// so try to stop the sketch.
try {
global.noLoop();
}
catch (e) { }
errorCb(message, line);
});
loadScripts([p5url(p5version)], function () {
document.body.appendChild(sketchScript);
if (document.readyState === 'complete') {
try {
new global.p5();
}
catch (e) {
console.error('Failed to initialize p5:', e);
}
}
});
}
global.startSketch = startSketch;
/***/ }),
/***/ 210:
/***/ (function(module, exports) {
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
// css base code, injected by the css-loader
module.exports = function() {
var list = [];
// return the list of modules as css string
list.toString = function toString() {
var result = [];
for(var i = 0; i < this.length; i++) {
var item = this[i];
if(item[2]) {
result.push("@media " + item[2] + "{" + item[1] + "}");
} else {
result.push(item[1]);
}
}
return result.join("");
};
// import a list of modules into the list
list.i = function(modules, mediaQuery) {
if(typeof modules === "string")
modules = [[null, modules, ""]];
var alreadyImportedModules = {};
for(var i = 0; i < this.length; i++) {
var id = this[i][0];
if(typeof id === "number")
alreadyImportedModules[id] = true;
}
for(i = 0; i < modules.length; i++) {
var item = modules[i];
// skip already imported module
// this implementation is not 100% perfect for weird media query combinations
// when a module is imported multiple times with different media queries.
// I hope this will never occur (Hey this way we have smaller bundles)
if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) {
if(mediaQuery && !item[2]) {
item[2] = mediaQuery;
} else if(mediaQuery) {
item[2] = "(" + item[2] + ") and (" + mediaQuery + ")";
}
list.push(item);
}
}
};
return list;
};
/***/ }),
/***/ 211:
/***/ (function(module, exports, __webpack_require__) {
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
var stylesInDom = {},
memoize = function(fn) {
var memo;
return function () {
if (typeof memo === "undefined") memo = fn.apply(this, arguments);
return memo;
};
},
isOldIE = memoize(function() {
return /msie [6-9]\b/.test(self.navigator.userAgent.toLowerCase());
}),
getHeadElement = memoize(function () {
return document.head || document.getElementsByTagName("head")[0];
}),
singletonElement = null,
singletonCounter = 0,
styleElementsInsertedAtTop = [];
module.exports = function(list, options) {
if(false) {
if(typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment");
}
options = options || {};
// Force single-tag solution on IE6-9, which has a hard limit on the # of <style>
// tags it will allow on a page
if (typeof options.singleton === "undefined") options.singleton = isOldIE();
// By default, add <style> tags to the bottom of <head>.
if (typeof options.insertAt === "undefined") options.insertAt = "bottom";
var styles = listToStyles(list);
addStylesToDom(styles, options);
return function update(newList) {
var mayRemove = [];
for(var i = 0; i < styles.length; i++) {
var item = styles[i];
var domStyle = stylesInDom[item.id];
domStyle.refs--;
mayRemove.push(domStyle);
}
if(newList) {
var newStyles = listToStyles(newList);
addStylesToDom(newStyles, options);
}
for(var i = 0; i < mayRemove.length; i++) {
var domStyle = mayRemove[i];
if(domStyle.refs === 0) {
for(var j = 0; j < domStyle.parts.length; j++)
domStyle.parts[j]();
delete stylesInDom[domStyle.id];
}
}
};
}
function addStylesToDom(styles, options) {
for(var i = 0; i < styles.length; i++) {
var item = styles[i];
var domStyle = stylesInDom[item.id];
if(domStyle) {
domStyle.refs++;
for(var j = 0; j < domStyle.parts.length; j++) {
domStyle.parts[j](item.parts[j]);
}
for(; j < item.parts.length; j++) {
domStyle.parts.push(addStyle(item.parts[j], options));
}
} else {
var parts = [];
for(var j = 0; j < item.parts.length; j++) {
parts.push(addStyle(item.parts[j], options));
}
stylesInDom[item.id] = {id: item.id, refs: 1, parts: parts};
}
}
}
function listToStyles(list) {
var styles = [];
var newStyles = {};
for(var i = 0; i < list.length; i++) {
var item = list[i];
var id = item[0];
var css = item[1];
var media = item[2];
var sourceMap = item[3];
var part = {css: css, media: media, sourceMap: sourceMap};
if(!newStyles[id])
styles.push(newStyles[id] = {id: id, parts: [part]});
else
newStyles[id].parts.push(part);
}
return styles;
}
function insertStyleElement(options, styleElement) {
var head = getHeadElement();
var lastStyleElementInsertedAtTop = styleElementsInsertedAtTop[styleElementsInsertedAtTop.length - 1];
if (options.insertAt === "top") {
if(!lastStyleElementInsertedAtTop) {
head.insertBefore(styleElement, head.firstChild);
} else if(lastStyleElementInsertedAtTop.nextSibling) {
head.insertBefore(styleElement, lastStyleElementInsertedAtTop.nextSibling);
} else {
head.appendChild(styleElement);
}
styleElementsInsertedAtTop.push(styleElement);
} else if (options.insertAt === "bottom") {
head.appendChild(styleElement);
} else {
throw new Error("Invalid value for parameter 'insertAt'. Must be 'top' or 'bottom'.");
}
}
function removeStyleElement(styleElement) {
styleElement.parentNode.removeChild(styleElement);
var idx = styleElementsInsertedAtTop.indexOf(styleElement);
if(idx >= 0) {
styleElementsInsertedAtTop.splice(idx, 1);
}
}
function createStyleElement(options) {
var styleElement = document.createElement("style");
styleElement.type = "text/css";
insertStyleElement(options, styleElement);
return styleElement;
}
function createLinkElement(options) {
var linkElement = document.createElement("link");
linkElement.rel = "stylesheet";
insertStyleElement(options, linkElement);
return linkElement;
}
function addStyle(obj, options) {
var styleElement, update, remove;
if (options.singleton) {
var styleIndex = singletonCounter++;
styleElement = singletonElement || (singletonElement = createStyleElement(options));
update = applyToSingletonTag.bind(null, styleElement, styleIndex, false);
remove = applyToSingletonTag.bind(null, styleElement, styleIndex, true);
} else if(obj.sourceMap &&
typeof URL === "function" &&
typeof URL.createObjectURL === "function" &&
typeof URL.revokeObjectURL === "function" &&
typeof Blob === "function" &&
typeof btoa === "function") {
styleElement = createLinkElement(options);
update = updateLink.bind(null, styleElement);
remove = function() {
removeStyleElement(styleElement);
if(styleElement.href)
URL.revokeObjectURL(styleElement.href);
};
} else {
styleElement = createStyleElement(options);
update = applyToTag.bind(null, styleElement);
remove = function() {
removeStyleElement(styleElement);
};
}
update(obj);
return function updateStyle(newObj) {
if(newObj) {
if(newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap)
return;
update(obj = newObj);
} else {
remove();
}
};
}
var replaceText = (function () {
var textStore = [];
return function (index, replacement) {
textStore[index] = replacement;
return textStore.filter(Boolean).join('\n');
};
})();
function applyToSingletonTag(styleElement, index, remove, obj) {
var css = remove ? "" : obj.css;
if (styleElement.styleSheet) {
styleElement.styleSheet.cssText = replaceText(index, css);
} else {
var cssNode = document.createTextNode(css);
var childNodes = styleElement.childNodes;
if (childNodes[index]) styleElement.removeChild(childNodes[index]);
if (childNodes.length) {
styleElement.insertBefore(cssNode, childNodes[index]);
} else {
styleElement.appendChild(cssNode);
}
}
}
function applyToTag(styleElement, obj) {
var css = obj.css;
var media = obj.media;
if(media) {
styleElement.setAttribute("media", media)
}
if(styleElement.styleSheet) {
styleElement.styleSheet.cssText = css;
} else {
while(styleElement.firstChild) {
styleElement.removeChild(styleElement.firstChild);
}
styleElement.appendChild(document.createTextNode(css));
}
}
function updateLink(linkElement, obj) {
var css = obj.css;
var sourceMap = obj.sourceMap;
if(sourceMap) {
// http://stackoverflow.com/a/26603875
css += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + " */";
}
var blob = new Blob([css], { type: "text/css" });
var oldSrc = linkElement.href;
linkElement.href = URL.createObjectURL(blob);
if(oldSrc)
URL.revokeObjectURL(oldSrc);
}
/***/ }),
/***/ 216:
/***/ (function(module, exports, __webpack_require__) {
// style-loader: Adds some css to the DOM by adding a <style> tag
// load the styles
var content = __webpack_require__(217);
if(typeof content === 'string') content = [[module.id, content, '']];
// add the styles to the DOM
var update = __webpack_require__(211)(content, {});
if(content.locals) module.exports = content.locals;
// Hot Module Replacement
if(false) {
// When the styles change, update the <style> tags
if(!content.locals) {
module.hot.accept("!!../node_modules/.store/css-loader@0.23.1/node_modules/css-loader/index.js?sourceMap!../node_modules/.store/postcss-loader@0.8.2/node_modules/postcss-loader/index.js?sourceMap!./preview-frame.css", function() {
var newContent = require("!!../node_modules/.store/css-loader@0.23.1/node_modules/css-loader/index.js?sourceMap!../node_modules/.store/postcss-loader@0.8.2/node_modules/postcss-loader/index.js?sourceMap!./preview-frame.css");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
update(newContent);
});
}
// When the module is disposed, remove the <style> tags
module.hot.dispose(function() { update(); });
}
/***/ }),
/***/ 217:
/***/ (function(module, exports, __webpack_require__) {
exports = module.exports = __webpack_require__(210)();
// imports
// module
exports.push([module.id, "html, body {\r\n height: 100%;\r\n}\r\n\r\nbody {\r\n margin: 0;\r\n display: -ms-flexbox;\r\n display: flex;\r\n\r\n /* This centers our sketch horizontally. */\r\n -ms-flex-pack: center;\r\n justify-content: center;\r\n\r\n /* This centers our sketch vertically. */\r\n -ms-flex-align: center;\r\n align-items: center;\r\n}\r\n", "", {"version":3,"sources":["/./css/preview-frame.css"],"names":[],"mappings":"AAAA;EACE,aAAa;CACd;;AAED;EACE,UAAU;EACV,qBAAc;EAAd,cAAc;;EAEd,2CAA2C;EAC3C,sBAAwB;MAAxB,wBAAwB;;EAExB,yCAAyC;EACzC,uBAAoB;MAApB,oBAAoB;CACrB","file":"preview-frame.css","sourcesContent":["html, body {\r\n height: 100%;\r\n}\r\n\r\nbody {\r\n margin: 0;\r\n display: flex;\r\n\r\n /* This centers our sketch horizontally. */\r\n justify-content: center;\r\n\r\n /* This centers our sketch vertically. */\r\n align-items: center;\r\n}\r\n"],"sourceRoot":"webpack://"}]);
// exports
/***/ })
/******/ });
//# sourceMappingURL=preview-frame.bundle.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Preview</title>
<body>
<!-- <script src="./p5.js"></script> -->
<script src="./src/CCapture.js"></script>
<script>
let capturer = new CCapture({
format: 'png',
framerate: 60,
verbose: true
});
var capturer_start = (t = 1) => {
if (frameCount === t && capturer) {
capturer.start();
}
}
var capturer_end = (t = 24) => {
// console.log(frameCount < t, frameCount, t)
if (frameCount < t && capturer) {
capturer.capture(canvas, t);
} else if (capturer) {
capturer.save((frames) => {
window.parent.postMessage(
{
frames,
from: 'p5.widget',
status: 'save',
encoder: true
},
'*'
)
});
capturer.stop();
capturer = null;
}
}
// 监听来自iframe的消息
window.addEventListener('message', (event) => {
const data = event.data;
if (data.from === 'p5.widget' && data.status === 'stop') {
window.location.reload()
}
});
</script>
<script src="./preview-frame.bundle.js"></script>
</body>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="250px" height="114px" viewBox="0 0 250 114" enable-background="new 0 0 250 114" xml:space="preserve">
<path fill="#EC245E" d="M16.254,27.631v7.998h0.359c0.715-1.113,1.65-2.248,2.805-3.402c1.155-1.154,2.568-2.188,4.24-3.105
c1.67-0.912,3.561-1.67,5.67-2.268c2.107-0.596,4.477-0.896,7.104-0.896c4.059,0,7.799,0.777,11.223,2.328
c3.422,1.555,6.367,3.684,8.836,6.389c2.465,2.707,4.375,5.891,5.73,9.551c1.352,3.662,2.029,7.602,2.029,11.82
s-0.656,8.179-1.971,11.879c-1.312,3.701-3.184,6.925-5.611,9.67c-2.427,2.746-5.371,4.938-8.834,6.566
c-3.463,1.631-7.385,2.446-11.76,2.446c-4.061,0-7.781-0.836-11.164-2.506c-3.385-1.672-5.99-3.938-7.82-6.807h-0.238v36.295H2.525
V27.631H16.254z M49.684,56.045c0-2.229-0.338-4.438-1.014-6.627c-0.678-2.188-1.693-4.158-3.045-5.91
c-1.354-1.748-3.064-3.162-5.135-4.238c-2.07-1.074-4.496-1.611-7.281-1.611c-2.627,0-4.977,0.557-7.045,1.672
c-2.07,1.115-3.842,2.549-5.312,4.297c-1.475,1.752-2.588,3.742-3.344,5.971c-0.758,2.229-1.133,4.459-1.133,6.686
c0,2.229,0.375,4.438,1.133,6.625c0.756,2.191,1.869,4.16,3.344,5.912c1.471,1.75,3.242,3.164,5.312,4.236
c2.068,1.075,4.418,1.61,7.045,1.61c2.785,0,5.211-0.555,7.281-1.67c2.07-1.115,3.781-2.547,5.135-4.299
c1.352-1.75,2.367-3.74,3.045-5.97C49.346,60.502,49.684,58.273,49.684,56.045z M189.332,24.893v63.505
c0,3.422-0.279,6.666-0.836,9.73c-0.559,3.064-1.611,5.73-3.164,8c-1.551,2.27-3.662,4.078-6.328,5.432
c-2.668,1.354-6.146,2.029-10.445,2.029c-1.193,0-2.389-0.08-3.582-0.238c-1.193-0.16-2.148-0.319-2.865-0.479l1.195-12.178
c0.637,0.16,1.311,0.279,2.027,0.359c0.717,0.077,1.354,0.118,1.91,0.118c1.67,0,3.023-0.317,4.059-0.955
c1.033-0.639,1.83-1.514,2.391-2.627c0.555-1.114,0.914-2.407,1.074-3.881c0.156-1.474,0.236-3.043,0.236-4.715V24.893H189.332z
M238.162,42.912c-1.275-1.672-3.025-3.123-5.254-4.357c-2.229-1.234-4.656-1.852-7.283-1.852c-2.309,0-4.416,0.479-6.326,1.434
c-1.912,0.953-2.863,2.547-2.863,4.775s1.053,3.803,3.16,4.715c2.109,0.916,5.195,1.852,9.256,2.807
c2.146,0.479,4.314,1.115,6.506,1.91c2.189,0.795,4.18,1.85,5.971,3.164c1.789,1.312,3.242,2.945,4.357,4.895
c1.111,1.951,1.672,4.318,1.672,7.104c0,3.504-0.658,6.47-1.973,8.896c-1.311,2.428-3.062,4.397-5.254,5.91
c-2.189,1.512-4.734,2.606-7.641,3.283c-2.906,0.676-5.908,1.014-9.014,1.014c-4.459,0-8.795-0.816-13.014-2.447
c-4.219-1.629-7.721-3.959-10.506-6.982l9.432-8.836c1.592,2.07,3.66,3.781,6.209,5.133c2.547,1.354,5.371,2.029,8.477,2.029
c1.033,0,2.088-0.117,3.164-0.357c1.074-0.237,2.068-0.614,2.984-1.133c0.914-0.518,1.65-1.213,2.209-2.09
c0.555-0.877,0.834-1.949,0.834-3.225c0-2.389-1.094-4.098-3.281-5.133c-2.191-1.035-5.475-2.07-9.85-3.104
c-2.15-0.479-4.24-1.094-6.27-1.853c-2.029-0.756-3.84-1.75-5.432-2.983c-1.596-1.234-2.865-2.764-3.82-4.598
c-0.955-1.83-1.436-4.098-1.436-6.805c0-3.184,0.656-5.928,1.973-8.236c1.311-2.312,3.045-4.197,5.191-5.674
c2.148-1.471,4.576-2.566,7.283-3.281c2.705-0.717,5.492-1.076,8.357-1.076c4.137,0,8.178,0.717,12.117,2.148
c3.939,1.434,7.062,3.625,9.373,6.568L238.162,42.912z M153.559,72.816l8.533-2.576l1.676,5.156l-8.498,2.897l5.275,7.479
l-4.447,3.226l-5.553-7.349l-5.408,7.154l-4.318-3.289l5.275-7.223l-8.564-3.09l1.678-5.16l8.6,2.771v-8.896h5.754v8.897H153.559z
M124.086,45.836c-1.473-3.301-3.52-6.088-6.148-8.357c-2.625-2.268-5.711-4-9.252-5.193c-3.543-1.193-7.383-1.791-11.521-1.791
c-1.512,0-3.203,0.082-5.074,0.238c-1.871,0.162-3.482,0.439-4.834,0.838l0.834-18.268h34.503V0.41H74.481l-1.432,46.201
c1.271-0.635,2.725-1.232,4.357-1.791c1.631-0.555,3.301-1.053,5.014-1.49c1.711-0.438,3.463-0.775,5.254-1.016
c1.791-0.238,3.48-0.357,5.074-0.357c2.307,0,4.576,0.258,6.805,0.775c2.227,0.518,4.238,1.434,6.029,2.746s3.242,3.045,4.357,5.193
c1.113,2.148,1.672,4.855,1.672,8.119c0,2.547-0.418,4.836-1.254,6.865c-0.836,2.026-1.971,3.721-3.402,5.071
c-1.434,1.355-3.104,2.39-5.016,3.104c-1.91,0.719-3.939,1.076-6.088,1.076c-3.82,0-7.125-1.017-9.91-3.046
c-2.787-2.028-4.775-4.715-5.969-8.059l-0.16,0.059l-10.367,9.716c2.096,3.42,4.799,6.28,8.139,8.553
c4.854,3.302,10.824,4.955,17.91,4.955c4.217,0,8.197-0.678,11.938-2.028c3.741-1.352,7.004-3.304,9.791-5.853
c2.786-2.545,4.994-5.67,6.627-9.371c1.629-3.701,2.445-7.897,2.445-12.597C126.295,52.939,125.559,49.141,124.086,45.836z
M131.07,6.842h2.521c0.244,0,0.484,0.029,0.723,0.086c0.236,0.059,0.447,0.152,0.635,0.283c0.186,0.131,0.336,0.301,0.453,0.508
c0.115,0.207,0.172,0.457,0.172,0.749c0,0.365-0.104,0.667-0.311,0.904c-0.207,0.237-0.479,0.407-0.812,0.511v0.02
c0.408,0.055,0.742,0.213,1.006,0.475c0.262,0.262,0.393,0.611,0.393,1.051c0,0.354-0.07,0.65-0.209,0.891
c-0.143,0.24-0.324,0.434-0.555,0.58c-0.229,0.146-0.488,0.251-0.785,0.314c-0.295,0.064-0.596,0.096-0.898,0.096h-2.33V6.842
H131.07z M132.221,9.473h1.023c0.383,0,0.676-0.076,0.877-0.229c0.201-0.153,0.301-0.369,0.301-0.648c0-0.293-0.104-0.5-0.311-0.621
c-0.207-0.122-0.529-0.184-0.969-0.184h-0.924v1.682H132.221z M132.221,12.341h1.031c0.146,0,0.307-0.011,0.477-0.032
s0.328-0.064,0.471-0.133c0.143-0.066,0.262-0.164,0.355-0.292c0.096-0.128,0.143-0.298,0.143-0.511
c0-0.342-0.115-0.579-0.348-0.713c-0.23-0.135-0.582-0.201-1.051-0.201h-1.078V12.341z M136.936,6.842h4.283v1.004h-3.135v1.645
h2.969v0.969h-2.969v1.827h3.299v1.022h-4.447V6.842z M144.088,7.846h-1.982V6.842h5.117v1.004h-1.982v5.463h-1.152V7.846
L144.088,7.846z M149.449,6.842h0.996l2.787,6.467h-1.316l-0.602-1.479h-2.807l-0.584,1.479h-1.289L149.449,6.842z M150.912,10.843
l-0.996-2.631l-1.014,2.631H150.912z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -0,0 +1,17 @@
html {
min-width: 768px;
}
body {
font-family: Georgia, serif;
max-width: 740px;
margin: 0 auto;
}
h1, h2, h3 {
font-weight: normal;
}
a {
color: inherit;
}
+338
View File
@@ -0,0 +1,338 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-ruler {
border-left: 1px solid #ccc;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
/* Hack to make IE7 behave */
*zoom:1;
*display:inline;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor { position: absolute; }
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
.CodeMirror span { *vertical-align: text-bottom; }
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }
File diff suppressed because it is too large Load Diff
+123
View File
@@ -0,0 +1,123 @@
/* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript */
/**
* okaidia theme for JavaScript, CSS and HTML
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
* @author ocodia
*/
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #272822;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #f8f8f2;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #f92672;
}
.token.boolean,
.token.number {
color: #ae81ff;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #a6e22e;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function {
color: #e6db74;
}
.token.keyword {
color: #66d9ef;
}
.token.regex,
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
+668
View File
@@ -0,0 +1,668 @@
/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */
var _self = (typeof window !== 'undefined')
? window // if in browser
: (
(typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
? self // if in worker
: {} // if in node js
);
/**
* Prism: Lightweight, robust, elegant syntax highlighting
* MIT license http://www.opensource.org/licenses/mit-license.php/
* @author Lea Verou http://lea.verou.me
*/
var Prism = (function(){
// Private helper vars
var lang = /\blang(?:uage)?-(\w+)\b/i;
var uniqueId = 0;
var _ = _self.Prism = {
util: {
encode: function (tokens) {
if (tokens instanceof Token) {
return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias);
} else if (_.util.type(tokens) === 'Array') {
return tokens.map(_.util.encode);
} else {
return tokens.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
}
},
type: function (o) {
return Object.prototype.toString.call(o).match(/\[object (\w+)\]/)[1];
},
objId: function (obj) {
if (!obj['__id']) {
Object.defineProperty(obj, '__id', { value: ++uniqueId });
}
return obj['__id'];
},
// Deep clone a language definition (e.g. to extend it)
clone: function (o) {
var type = _.util.type(o);
switch (type) {
case 'Object':
var clone = {};
for (var key in o) {
if (o.hasOwnProperty(key)) {
clone[key] = _.util.clone(o[key]);
}
}
return clone;
case 'Array':
// Check for existence for IE8
return o.map && o.map(function(v) { return _.util.clone(v); });
}
return o;
}
},
languages: {
extend: function (id, redef) {
var lang = _.util.clone(_.languages[id]);
for (var key in redef) {
lang[key] = redef[key];
}
return lang;
},
/**
* Insert a token before another token in a language literal
* As this needs to recreate the object (we cannot actually insert before keys in object literals),
* we cannot just provide an object, we need anobject and a key.
* @param inside The key (or language id) of the parent
* @param before The key to insert before. If not provided, the function appends instead.
* @param insert Object with the key/value pairs to insert
* @param root The object that contains `inside`. If equal to Prism.languages, it can be omitted.
*/
insertBefore: function (inside, before, insert, root) {
root = root || _.languages;
var grammar = root[inside];
if (arguments.length == 2) {
insert = arguments[1];
for (var newToken in insert) {
if (insert.hasOwnProperty(newToken)) {
grammar[newToken] = insert[newToken];
}
}
return grammar;
}
var ret = {};
for (var token in grammar) {
if (grammar.hasOwnProperty(token)) {
if (token == before) {
for (var newToken in insert) {
if (insert.hasOwnProperty(newToken)) {
ret[newToken] = insert[newToken];
}
}
}
ret[token] = grammar[token];
}
}
// Update references in other language definitions
_.languages.DFS(_.languages, function(key, value) {
if (value === root[inside] && key != inside) {
this[key] = ret;
}
});
return root[inside] = ret;
},
// Traverse a language definition with Depth First Search
DFS: function(o, callback, type, visited) {
visited = visited || {};
for (var i in o) {
if (o.hasOwnProperty(i)) {
callback.call(o, i, o[i], type || i);
if (_.util.type(o[i]) === 'Object' && !visited[_.util.objId(o[i])]) {
visited[_.util.objId(o[i])] = true;
_.languages.DFS(o[i], callback, null, visited);
}
else if (_.util.type(o[i]) === 'Array' && !visited[_.util.objId(o[i])]) {
visited[_.util.objId(o[i])] = true;
_.languages.DFS(o[i], callback, i, visited);
}
}
}
}
},
plugins: {},
highlightAll: function(async, callback) {
var env = {
callback: callback,
selector: 'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'
};
_.hooks.run("before-highlightall", env);
var elements = env.elements || document.querySelectorAll(env.selector);
for (var i=0, element; element = elements[i++];) {
_.highlightElement(element, async === true, env.callback);
}
},
highlightElement: function(element, async, callback) {
// Find language
var language, grammar, parent = element;
while (parent && !lang.test(parent.className)) {
parent = parent.parentNode;
}
if (parent) {
language = (parent.className.match(lang) || [,''])[1];
grammar = _.languages[language];
}
// Set language on the element, if not present
element.className = element.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
// Set language on the parent, for styling
parent = element.parentNode;
if (/pre/i.test(parent.nodeName)) {
parent.className = parent.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
}
var code = element.textContent;
var env = {
element: element,
language: language,
grammar: grammar,
code: code
};
if (!code || !grammar) {
_.hooks.run('complete', env);
return;
}
_.hooks.run('before-highlight', env);
if (async && _self.Worker) {
var worker = new Worker(_.filename);
worker.onmessage = function(evt) {
env.highlightedCode = evt.data;
_.hooks.run('before-insert', env);
env.element.innerHTML = env.highlightedCode;
callback && callback.call(env.element);
_.hooks.run('after-highlight', env);
_.hooks.run('complete', env);
};
worker.postMessage(JSON.stringify({
language: env.language,
code: env.code,
immediateClose: true
}));
}
else {
env.highlightedCode = _.highlight(env.code, env.grammar, env.language);
_.hooks.run('before-insert', env);
env.element.innerHTML = env.highlightedCode;
callback && callback.call(element);
_.hooks.run('after-highlight', env);
_.hooks.run('complete', env);
}
},
highlight: function (text, grammar, language) {
var tokens = _.tokenize(text, grammar);
return Token.stringify(_.util.encode(tokens), language);
},
tokenize: function(text, grammar, language) {
var Token = _.Token;
var strarr = [text];
var rest = grammar.rest;
if (rest) {
for (var token in rest) {
grammar[token] = rest[token];
}
delete grammar.rest;
}
tokenloop: for (var token in grammar) {
if(!grammar.hasOwnProperty(token) || !grammar[token]) {
continue;
}
var patterns = grammar[token];
patterns = (_.util.type(patterns) === "Array") ? patterns : [patterns];
for (var j = 0; j < patterns.length; ++j) {
var pattern = patterns[j],
inside = pattern.inside,
lookbehind = !!pattern.lookbehind,
greedy = !!pattern.greedy,
lookbehindLength = 0,
alias = pattern.alias;
pattern = pattern.pattern || pattern;
for (var i=0; i<strarr.length; i++) { // Don’t cache length as it changes during the loop
var str = strarr[i];
if (strarr.length > text.length) {
// Something went terribly wrong, ABORT, ABORT!
break tokenloop;
}
if (str instanceof Token) {
continue;
}
pattern.lastIndex = 0;
var match = pattern.exec(str),
delNum = 1;
// Greedy patterns can override/remove up to two previously matched tokens
if (!match && greedy && i != strarr.length - 1) {
// Reconstruct the original text using the next two tokens
var nextToken = strarr[i + 1].matchedStr || strarr[i + 1],
combStr = str + nextToken;
if (i < strarr.length - 2) {
combStr += strarr[i + 2].matchedStr || strarr[i + 2];
}
// Try the pattern again on the reconstructed text
pattern.lastIndex = 0;
match = pattern.exec(combStr);
if (!match) {
continue;
}
var from = match.index + (lookbehind ? match[1].length : 0);
// To be a valid candidate, the new match has to start inside of str
if (from >= str.length) {
continue;
}
var to = match.index + match[0].length,
len = str.length + nextToken.length;
// Number of tokens to delete and replace with the new match
delNum = 3;
if (to <= len) {
if (strarr[i + 1].greedy) {
continue;
}
delNum = 2;
combStr = combStr.slice(0, len);
}
str = combStr;
}
if (!match) {
continue;
}
if(lookbehind) {
lookbehindLength = match[1].length;
}
var from = match.index + lookbehindLength,
match = match[0].slice(lookbehindLength),
to = from + match.length,
before = str.slice(0, from),
after = str.slice(to);
var args = [i, delNum];
if (before) {
args.push(before);
}
var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias, match, greedy);
args.push(wrapped);
if (after) {
args.push(after);
}
Array.prototype.splice.apply(strarr, args);
}
}
}
return strarr;
},
hooks: {
all: {},
add: function (name, callback) {
var hooks = _.hooks.all;
hooks[name] = hooks[name] || [];
hooks[name].push(callback);
},
run: function (name, env) {
var callbacks = _.hooks.all[name];
if (!callbacks || !callbacks.length) {
return;
}
for (var i=0, callback; callback = callbacks[i++];) {
callback(env);
}
}
}
};
var Token = _.Token = function(type, content, alias, matchedStr, greedy) {
this.type = type;
this.content = content;
this.alias = alias;
// Copy of the full string this token was created from
this.matchedStr = matchedStr || null;
this.greedy = !!greedy;
};
Token.stringify = function(o, language, parent) {
if (typeof o == 'string') {
return o;
}
if (_.util.type(o) === 'Array') {
return o.map(function(element) {
return Token.stringify(element, language, o);
}).join('');
}
var env = {
type: o.type,
content: Token.stringify(o.content, language, parent),
tag: 'span',
classes: ['token', o.type],
attributes: {},
language: language,
parent: parent
};
if (env.type == 'comment') {
env.attributes['spellcheck'] = 'true';
}
if (o.alias) {
var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias];
Array.prototype.push.apply(env.classes, aliases);
}
_.hooks.run('wrap', env);
var attributes = '';
for (var name in env.attributes) {
attributes += (attributes ? ' ' : '') + name + '="' + (env.attributes[name] || '') + '"';
}
return '<' + env.tag + ' class="' + env.classes.join(' ') + '" ' + attributes + '>' + env.content + '</' + env.tag + '>';
};
if (!_self.document) {
if (!_self.addEventListener) {
// in Node.js
return _self.Prism;
}
// In worker
_self.addEventListener('message', function(evt) {
var message = JSON.parse(evt.data),
lang = message.language,
code = message.code,
immediateClose = message.immediateClose;
_self.postMessage(_.highlight(code, _.languages[lang], lang));
if (immediateClose) {
_self.close();
}
}, false);
return _self.Prism;
}
//Get current script and highlight
var script = document.currentScript || [].slice.call(document.getElementsByTagName("script")).pop();
if (script) {
_.filename = script.src;
if (document.addEventListener && !script.hasAttribute('data-manual')) {
document.addEventListener('DOMContentLoaded', _.highlightAll);
}
}
return _self.Prism;
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = Prism;
}
// hack for components to work correctly in node.js
if (typeof global !== 'undefined') {
global.Prism = Prism;
}
;
Prism.languages.markup = {
'comment': /<!--[\w\W]*?-->/,
'prolog': /<\?[\w\W]+?\?>/,
'doctype': /<!DOCTYPE[\w\W]+?>/,
'cdata': /<!\[CDATA\[[\w\W]*?]]>/i,
'tag': {
pattern: /<\/?(?!\d)[^\s>\/=.$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,
inside: {
'tag': {
pattern: /^<\/?[^\s>\/]+/i,
inside: {
'punctuation': /^<\/?/,
'namespace': /^[^\s>\/:]+:/
}
},
'attr-value': {
pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,
inside: {
'punctuation': /[=>"']/
}
},
'punctuation': /\/?>/,
'attr-name': {
pattern: /[^\s>\/]+/,
inside: {
'namespace': /^[^\s>\/:]+:/
}
}
}
},
'entity': /&#?[\da-z]{1,8};/i
};
// Plugin to make entity title show the real entity, idea by Roman Komarov
Prism.hooks.add('wrap', function(env) {
if (env.type === 'entity') {
env.attributes['title'] = env.content.replace(/&amp;/, '&');
}
});
Prism.languages.xml = Prism.languages.markup;
Prism.languages.html = Prism.languages.markup;
Prism.languages.mathml = Prism.languages.markup;
Prism.languages.svg = Prism.languages.markup;
Prism.languages.css = {
'comment': /\/\*[\w\W]*?\*\//,
'atrule': {
pattern: /@[\w-]+?.*?(;|(?=\s*\{))/i,
inside: {
'rule': /@[\w-]+/
// See rest below
}
},
'url': /url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
'selector': /[^\{\}\s][^\{\};]*?(?=\s*\{)/,
'string': /("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,
'property': /(\b|\B)[\w-]+(?=\s*:)/i,
'important': /\B!important\b/i,
'function': /[-a-z0-9]+(?=\()/i,
'punctuation': /[(){};:]/
};
Prism.languages.css['atrule'].inside.rest = Prism.util.clone(Prism.languages.css);
if (Prism.languages.markup) {
Prism.languages.insertBefore('markup', 'tag', {
'style': {
pattern: /(<style[\w\W]*?>)[\w\W]*?(?=<\/style>)/i,
lookbehind: true,
inside: Prism.languages.css,
alias: 'language-css'
}
});
Prism.languages.insertBefore('inside', 'attr-value', {
'style-attr': {
pattern: /\s*style=("|').*?\1/i,
inside: {
'attr-name': {
pattern: /^\s*style/i,
inside: Prism.languages.markup.tag.inside
},
'punctuation': /^\s*=\s*['"]|['"]\s*$/,
'attr-value': {
pattern: /.+/i,
inside: Prism.languages.css
}
},
alias: 'language-css'
}
}, Prism.languages.markup.tag);
};
Prism.languages.clike = {
'comment': [
{
pattern: /(^|[^\\])\/\*[\w\W]*?\*\//,
lookbehind: true
},
{
pattern: /(^|[^\\:])\/\/.*/,
lookbehind: true
}
],
'string': {
pattern: /(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
greedy: true
},
'class-name': {
pattern: /((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,
lookbehind: true,
inside: {
punctuation: /(\.|\\)/
}
},
'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,
'boolean': /\b(true|false)\b/,
'function': /[a-z0-9_]+(?=\()/i,
'number': /\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,
'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,
'punctuation': /[{}[\];(),.:]/
};
Prism.languages.javascript = Prism.languages.extend('clike', {
'keyword': /\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,
'number': /\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,
// Allow for all non-ASCII characters (See http://stackoverflow.com/a/2008444)
'function': /[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i
});
Prism.languages.insertBefore('javascript', 'keyword', {
'regex': {
pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,
lookbehind: true,
greedy: true
}
});
Prism.languages.insertBefore('javascript', 'class-name', {
'template-string': {
pattern: /`(?:\\\\|\\?[^\\])*?`/,
greedy: true,
inside: {
'interpolation': {
pattern: /\$\{[^}]+\}/,
inside: {
'interpolation-punctuation': {
pattern: /^\$\{|\}$/,
alias: 'punctuation'
},
rest: Prism.languages.javascript
}
},
'string': /[\s\S]+/
}
}
});
if (Prism.languages.markup) {
Prism.languages.insertBefore('markup', 'tag', {
'script': {
pattern: /(<script[\w\W]*?>)[\w\W]*?(?=<\/script>)/i,
lookbehind: true,
inside: Prism.languages.javascript,
alias: 'language-javascript'
}
});
}
Prism.languages.js = Prism.languages.javascript;
+138
View File
@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
height: 600px;
padding: 24px;
}
iframe {
min-height: 600px !important
}
/* 自定义滚动条样式 */
::-webkit-scrollbar {
width: 8px;
/* 滚动条宽度 */
}
::-webkit-scrollbar-track {
background: #f1f1f1;
/* 滚动条轨道颜色 */
}
::-webkit-scrollbar-thumb {
background: #888;
/* 滚动条滑块颜色 */
}
::-webkit-scrollbar-thumb:hover {
background: #555;
/* 滚动条滑块悬停颜色 */
}
</style>
</head>
<body>
<script>
function getIdFromUrl(url) {
const urlParams = new URLSearchParams(new URL(url).search);
return urlParams.get('id');
}
// 监听来自iframe的消息
window.addEventListener('message', (event) => {
const data = event.data;
const nodeId = getIdFromUrl(window.location.href);
// console.log('#p5html', data)
if (data.from === 'p5.widget' && data.status === 'save' && data._from == 'main') {
const frames = data.frames;
// 示例用法
// const url = 'https://example.com/page?id=12345';
window.parent.postMessage({
frames,
from: 'p5.widget',
status: 'save',
nodeId,
id:(new Date()).getTime()
}, '*');
// window.location.reload()
}
if (data.from === 'p5.widget' && data.status === 'stop') {
window.parent.postMessage({
from: 'p5.widget',
status: 'stop',
nodeId
}, '*');
// window.location.reload()
}
});
</script>
<script type="text/p5" data-height="500" data-preview-width="300" >
const TO_GOAL = 0.3
const NOISE_AMP = 3.1415 / 100
let g_width
let g_red
let g_green
let g_blue
function draw_triangle (angle) {
triangle(
cos(TWO_PI / 3 + angle) * g_width,
sin(TWO_PI / 3 + angle) * g_width,
cos((TWO_PI / 3) * 2 + angle) * g_width,
sin((TWO_PI / 3) * 2 + angle) * g_width,
cos((TWO_PI / 3) * 3 + angle) * g_width,
sin((TWO_PI / 3) * 3 + angle) * g_width
)
}
function setup () {
createCanvas(300, 300)
mouseX = 300 / 2
mouseY = 300 / 2
g_red = map(mouseX, 0, 300, 0, PI)
g_green = map(mouseX, 0, 300, 0, PI)
g_blue = map(mouseX, 0, 300, 0, PI)
g_width = min(300, 300) / 3
}
function draw () {
background(0)
push()
translate(300 / 2, 300 / 2)
blendMode(ADD)
g_red =
g_red +
(map(0, 0, windowWidth, 0, PI) - g_red) * TO_GOAL +
random(-NOISE_AMP, NOISE_AMP)
g_green =
g_green + (g_red - g_green) * TO_GOAL + random(-NOISE_AMP, NOISE_AMP)
g_blue = g_blue + (g_green - g_blue) * TO_GOAL + random(-NOISE_AMP, NOISE_AMP)
fill(255, 0, 0)
draw_triangle(g_red)
fill(0, 255, 0)
draw_triangle(g_green)
fill(0, 0, 255)
draw_triangle(g_blue)
pop()
}
</script>
<script src="./p5-widget/p5-widget.js"></script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
dialog {
width: 100%;
text-align: center;
max-width: 20em;
color: white;
background-color: #000;
border: none;
position: relative;
transform: translate(-50%, -50%);
}
#progress-container {
position: absolute;
top: 50%;
left: 50%;
}
progress {
width: 100%;
height: 1em;
border: none;
background-color: #fff;
color: #eee;
}
progress::-webkit-progress-bar {
background-color: #333;
}
progress::-webkit-progress-value {
background-color: #eee;
}
progress::-moz-progress-bar {
background-color: #eee;
}
+119
View File
@@ -0,0 +1,119 @@
body {
margin: 0;
background-color: #000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
}
a {
color: #ff0;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
cursor: pointer;
text-transform: uppercase;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
z-index: 1; /* TODO Solve this in HTML */
}
a, button, input, select {
pointer-events: auto;
}
.lil-gui {
z-index: 2 !important; /* TODO Solve this in HTML */
}
@media all and ( max-width: 640px ) {
.lil-gui.root {
right: auto;
top: auto;
max-height: 50%;
max-width: 80%;
bottom: 0;
left: 0;
}
}
#overlay {
position: absolute;
font-size: 16px;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgba(0,0,0,0.7);
}
#overlay button {
background: transparent;
border: 0;
border: 1px solid rgb(255, 255, 255);
border-radius: 4px;
color: #ffffff;
padding: 12px 18px;
text-transform: uppercase;
cursor: pointer;
}
#notSupported {
width: 50%;
margin: auto;
background-color: #f00;
margin-top: 20px;
padding: 10px;
}
#screenshotButton {
position: absolute;
bottom: 10px; /* Adjust as needed */
left: 10px; /* Position to the left */
z-index: 10; /* Ensure this is above the canvas's z-index */
padding: 5px 5px;
border: none;
border-radius: 5px;
background: linear-gradient(145deg, #007bff, #0056b3);
color: #ffffff;
font-size: 10px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background 0.3s ease-in-out, transform 0.2s ease;
}
#screenshotButton:hover {
background: linear-gradient(145deg, #0056b3, #007bff);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}
#screenshotButton:active {
background: #0056b3;
transform: translateY(1px);
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
}
+26
View File
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="/mixlab/app/style/threeStyle.css">
<link type="text/css" rel="stylesheet" href="/mixlab/app/style/progressStyle.css">
</head>
<body>
<div id="progress-container">
<dialog open id="progress-dialog">
<p>
<label for="progress-indicator">Loading scene...</label>
</p>
<progress max="100" id="progress-indicator"></progress>
</dialog>
</div>
<div id="container"></div>
<script id="visualizer" type="module" filepath="" crossorigin src="/mixlab/app/lib/threeVisualizer.js"></script>
<button id="screenshotButton">Take Screenshot</button>
</body>
</html>
+671 -435
View File
File diff suppressed because it is too large Load Diff
-631
View File
@@ -1,631 +0,0 @@
{
"last_node_id": 47,
"last_link_id": 46,
"nodes": [
{
"id": 27,
"type": "CLIPTextEncode",
"pos": [
1961.178268896482,
527.6060791015625
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 24
},
{
"name": "text",
"type": "STRING",
"link": 46,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
21
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 28,
"type": "CLIPTextEncode",
"pos": [
1958.7452854980445,
266
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 25
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
22
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 24,
"type": "KSampler",
"pos": [
2434.0233006347635,
80
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 20
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 21
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 22
},
{
"name": "latent_image",
"type": "LATENT",
"link": 23
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
26
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
971428736321335,
"randomize",
15,
8,
"euler",
"karras",
1
]
},
{
"id": 29,
"type": "VAEDecode",
"pos": [
2799.0233006347635,
80
],
"size": {
"0": 210,
"1": 46
},
"flags": {
"collapsed": false
},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 26
},
{
"name": "vae",
"type": "VAE",
"link": 27
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
31
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 26,
"type": "EmptyLatentImage",
"pos": [
2069.0233006347635,
80
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
23
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 25,
"type": "CheckpointLoaderSimple",
"pos": [
1593.7452854980445,
80
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
20
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
24,
25
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
27
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 31,
"type": "PreviewImage",
"pos": [
2439,
408
],
"size": {
"0": 563.4671630859375,
"1": 720.8866577148438
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 31
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 44,
"type": "ShowTextForGPT",
"pos": [
1171,
425
],
"size": {
"0": 432.46002197265625,
"1": 264.40771484375
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 44,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"[\n {\n \"role\": \"system\",\n \"content\": \"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"\"\n }\n]"
]
},
{
"id": 43,
"type": "ShowTextForGPT",
"pos": [
1166,
740
],
"size": {
"0": 424.0079650878906,
"1": 430.6391296386719
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 45,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"[\n {\n \"role\": \"user\",\n \"content\": \"\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"I'm sorry, I'm ChatGLM3-6B, not ChatGPT. I am a language model jointly trained by Tsinghua University KEG Lab and Zhipu AI Company.\"\n }\n]"
]
},
{
"id": 45,
"type": "ShowTextForGPT",
"pos": [
1092,
262
],
"size": {
"0": 635.8358154296875,
"1": 101.46092224121094
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 43,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"I'm sorry, I'm ChatGLM3-6B, not ChatGPT. I am a language model jointly trained by Tsinghua University KEG Lab and Zhipu AI Company."
]
},
{
"id": 47,
"type": "ChatGPTOpenAI",
"pos": [
585,
306
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
43,
46
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": [
44
],
"shape": 3,
"slot_index": 1
},
{
"name": "session_history",
"type": "STRING",
"links": [
45
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-3.5-turbo-16k",
2220,
"randomize",
1,
null
]
}
],
"links": [
[
2,
4,
0,
5,
0,
"STRING"
],
[
3,
6,
0,
4,
0,
"STRING"
],
[
4,
6,
1,
7,
0,
"JSON"
],
[
8,
10,
0,
11,
0,
"STRING"
],
[
12,
10,
1,
14,
0,
"STRING"
],
[
13,
10,
1,
16,
0,
"JSON"
],
[
20,
25,
0,
24,
0,
"MODEL"
],
[
21,
27,
0,
24,
1,
"CONDITIONING"
],
[
22,
28,
0,
24,
2,
"CONDITIONING"
],
[
23,
26,
0,
24,
3,
"LATENT"
],
[
24,
25,
1,
27,
0,
"CLIP"
],
[
25,
25,
1,
28,
0,
"CLIP"
],
[
26,
24,
0,
29,
0,
"LATENT"
],
[
27,
25,
2,
29,
1,
"VAE"
],
[
31,
29,
0,
31,
0,
"IMAGE"
],
[
43,
47,
0,
45,
0,
"STRING"
],
[
44,
47,
1,
44,
0,
"STRING"
],
[
45,
47,
2,
43,
0,
"STRING"
],
[
46,
47,
0,
27,
1,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
-251
View File
@@ -1,251 +0,0 @@
{
"last_node_id": 5,
"last_link_id": 4,
"nodes": [
{
"id": 4,
"type": "ChatGPTOpenAI",
"pos": [
-512,
-236
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
2
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"描述一个科幻的场景",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-35-turbo",
6933,
"randomize",
1,
null
]
},
{
"id": 2,
"type": "ChatGPTOpenAI",
"pos": [
-36,
-234
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 2,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
3
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"增加丰富的细节和光影,摄影技巧,镜头语言,材质肌理",
"gpt-3.5-turbo",
2836,
"randomize",
1,
null
]
},
{
"id": 5,
"type": "ChatGPTOpenAI",
"pos": [
-30,
183
],
"size": {
"0": 400,
"1": 342
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 3,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
4
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"翻译成英文,并按照格式输出: 画面、主题、细节、灯光、氛围、艺术家、其他",
"gpt-3.5-turbo",
1085,
"randomize",
1,
null
]
},
{
"id": 3,
"type": "ShowTextForGPT",
"pos": [
447,
-229
],
"size": [
503.79851499517997,
356.0560985581077
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 4,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"Picture: The picture is composed of carefully chosen elements, capturing the subject matter in a visually striking way.\n\nTheme: The theme of the photograph could vary, from capturing nature's beauty to showcasing urban landscapes, human emotions, or abstract concepts.\n\nDetails: The photograph captures intricate details, bringing attention to the subject's textures, colors, shapes, and patterns.\n\nLighting: The photographer manipulates lighting, using techniques like natural light, dramatic shadows, or artificial lighting to enhance the mood and atmosphere of the photograph.\n\nAmbiance: The photograph evokes a specific ambiance or mood, whether it's serene, mysterious, joyful, melancholic, or any other emotional response.\n\nArtist: The photographer skillfully crafts the image, demonstrating their artistic vision, technical skills, and creative expression through the composition, framing, and post-processing choices.\n\nOthers: Apart from the elements mentioned above, the photograph may also incorporate other creative techniques like long exposure, multiple exposures, color grading, or unconventional perspectives to create a unique and captivating image.\n\nIn the future, as technology and imagination continue to advance, photography will likely continue to evolve and innovate, offering even more realistic and awe-inspiring visual experiences for humans."
]
}
],
"links": [
[
2,
4,
0,
2,
0,
"STRING"
],
[
3,
2,
0,
5,
0,
"STRING"
],
[
4,
5,
0,
3,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
+408
View File
@@ -0,0 +1,408 @@
{
"last_node_id": 21,
"last_link_id": 16,
"nodes": [
{
"id": 10,
"type": "ChatGPTOpenAI",
"pos": [
489,
689
],
"size": {
"0": 403.2580261230469,
"1": 309.2166442871094
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "api_key",
"type": "STRING",
"link": null,
"widget": {
"name": "api_key"
}
},
{
"name": "custom_model_name",
"type": "STRING",
"link": null,
"widget": {
"name": "custom_model_name"
}
},
{
"name": "custom_api_url",
"type": "STRING",
"link": 13,
"widget": {
"name": "custom_api_url"
},
"slot_index": 2
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
12
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
"hi",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"gpt-3.5-turbo",
447210757728856,
"randomize",
1,
"openai",
"",
"",
""
]
},
{
"id": 3,
"type": "ShowTextForGPT",
"pos": [
982,
686
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 12,
"widget": {
"name": "text"
}
},
{
"name": "output_dir",
"type": "STRING",
"link": null,
"widget": {
"name": "output_dir"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"",
"",
" Hi there! What can I help you with?"
]
},
{
"id": 11,
"type": "SiliconflowLLM",
"pos": [
489,
318
],
"size": {
"0": 395.197998046875,
"1": 262
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "api_key",
"type": "STRING",
"link": 16,
"widget": {
"name": "api_key"
}
},
{
"name": "custom_model_name",
"type": "STRING",
"link": null,
"widget": {
"name": "custom_model_name"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
15
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "SiliconflowLLM"
},
"widgets_values": [
"",
"",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"Qwen/Qwen2-7B-Instruct",
593422808835285,
"randomize",
1,
""
]
},
{
"id": 17,
"type": "ShowTextForGPT",
"pos": [
975,
329
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 15,
"widget": {
"name": "text"
}
},
{
"name": "output_dir",
"type": "STRING",
"link": null,
"widget": {
"name": "output_dir"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "ShowTextForGPT"
},
"widgets_values": [
"",
"",
"Hello! How can I assist you today?"
]
},
{
"id": 18,
"type": "KeyInput",
"pos": [
46,
319
],
"size": {
"0": 315,
"1": 70
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "key",
"type": "STRING",
"links": [
16
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "KeyInput"
},
"widgets_values": [
null,
null
]
},
{
"id": 12,
"type": "TextInput_",
"pos": [
31,
871
],
"size": [
407.9377612789413,
76
],
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
13
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "TextInput_"
},
"widgets_values": [
"http://127.0.0.1:8000/v1"
]
},
{
"id": 20,
"type": "Note",
"pos": [
35,
664
],
"size": [
350.04604707424306,
116.54209784249178
],
"flags": {},
"order": 2,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"api_key 填写对应平台的Key\ncustom model和api 根据需要自行填写\n\n如果不填写custome,则按照model和api_url选择的选项"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 21,
"type": "Note",
"pos": [
42,
438
],
"size": {
"0": 350.0460510253906,
"1": 116.54209899902344
},
"flags": {},
"order": 3,
"mode": 0,
"properties": {
"text": ""
},
"widgets_values": [
"API key节点不会保存到workflow的json文件。\n\n::会保存到appinfo导出的app.json里\n\n\n注册https://cloud.siliconflow.cn/account/ak 领取免费的API"
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[
12,
10,
0,
3,
0,
"STRING"
],
[
13,
12,
0,
10,
2,
"STRING"
],
[
15,
11,
0,
17,
0,
"STRING"
],
[
16,
18,
0,
11,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.9646149645000006,
"offset": [
170.81398913081276,
-128.0066534315481
]
}
},
"version": 0.4
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More