Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
490fe73
1
Parent(s):
5a6c0fc
release
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .nvmrc +1 -1
- README.md +1 -1
- bun.lockb +0 -0
- documentation/RELEASE.md +16 -0
- package.json +4 -4
- packages/api-client/.gitignore +0 -177
- packages/api-client/.npmignore +0 -4
- packages/api-client/.prettierrc.json +0 -9
- packages/api-client/LICENSE.md +0 -21
- packages/api-client/README.md +0 -113
- packages/api-client/package.json +0 -46
- packages/api-client/src/api/createClap.ts +0 -43
- packages/api-client/src/api/editClapDialogues.ts +0 -61
- packages/api-client/src/api/editClapEntities.ts +0 -75
- packages/api-client/src/api/editClapMusic.ts +0 -60
- packages/api-client/src/api/editClapSounds.ts +0 -60
- packages/api-client/src/api/editClapStory.ts +0 -114
- packages/api-client/src/api/editClapStoryboards.ts +0 -60
- packages/api-client/src/api/editClapVideos.ts +0 -85
- packages/api-client/src/api/exportClapToVideo.ts +0 -60
- packages/api-client/src/api/index.ts +0 -9
- packages/api-client/src/constants/config.ts +0 -16
- packages/api-client/src/constants/defaultValues.ts +0 -10
- packages/api-client/src/constants/index.ts +0 -17
- packages/api-client/src/constants/types.ts +0 -25
- packages/api-client/src/index.ts +0 -28
- packages/api-client/src/parsers/index.ts +0 -3
- packages/api-client/src/parsers/parseEntityPrompt.ts +0 -30
- packages/api-client/src/parsers/parseString.ts +0 -7
- packages/api-client/src/parsers/parseStringArray.ts +0 -9
- packages/api-client/src/utils/applyClapCompletion.ts +0 -21
- packages/api-client/src/utils/index.ts +0 -1
- packages/api-client/tsconfig.json +0 -31
- packages/api-client/tsconfig.types.json +0 -13
- packages/app/.nvmrc +1 -1
- packages/app/package.json +19 -18
- packages/app/src/app/api/resolve/providers/aitube/index.ts +1 -1
- packages/app/src/app/api/resolve/providers/comfy-replicate/index.ts +1 -1
- packages/app/src/app/api/resolve/providers/comfy-replicate/runWorkflow.ts +5 -1
- packages/app/src/app/api/resolve/providers/comfyui/convertComfyUiWorkflowApiToClapWorkflow.ts +60 -0
- packages/app/src/app/api/resolve/providers/comfyui/createPromptBuilder.spec.ts +595 -0
- packages/app/src/app/api/resolve/providers/comfyui/createPromptBuilder.ts +42 -0
- packages/app/src/app/api/resolve/providers/comfyui/getInputsFromComfyUiWorkflow.ts +106 -0
- packages/app/src/app/api/resolve/providers/comfyui/getMainInputIdsByClapWorkflowCategory.ts +29 -0
- packages/app/src/app/api/resolve/providers/comfyui/getMainInputsFromComfyUiWorkflow.ts +259 -0
- packages/app/src/app/api/resolve/providers/comfyui/graph.ts +411 -0
- packages/app/src/app/api/resolve/providers/comfyui/index.ts +7 -6
- packages/app/src/app/api/resolve/providers/comfyui/{utils.spec.ts → tests.spec.ts} +3 -3
- packages/app/src/app/api/resolve/providers/comfyui/types.ts +39 -0
- packages/app/src/app/api/resolve/providers/comfyui/utils.ts +2 -905
.nvmrc
CHANGED
@@ -1 +1 @@
|
|
1 |
-
v20.
|
|
|
1 |
+
v20.17.0
|
README.md
CHANGED
@@ -80,7 +80,7 @@ git lfs install
|
|
80 |
|
81 |
You will also need to install [Bun](https://bun.sh/docs/installation)
|
82 |
|
83 |
-
Clapper has been tested with Node `20.
|
84 |
|
85 |
To make sure you use this version, you can use [NVM](https://github.com/nvm-sh/nvm) to activate it:
|
86 |
|
|
|
80 |
|
81 |
You will also need to install [Bun](https://bun.sh/docs/installation)
|
82 |
|
83 |
+
Clapper has been tested with Node `20.17.0`.
|
84 |
|
85 |
To make sure you use this version, you can use [NVM](https://github.com/nvm-sh/nvm) to activate it:
|
86 |
|
bun.lockb
CHANGED
Binary files a/bun.lockb and b/bun.lockb differ
|
|
documentation/RELEASE.md
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Release process
|
2 |
+
|
3 |
+
## Releasing the whole app
|
4 |
+
|
5 |
+
### Web version (push to clapper.app)
|
6 |
+
|
7 |
+
TODO
|
8 |
+
|
9 |
+
### Desktop version (build the Electron app)
|
10 |
+
|
11 |
+
## Releasing individual modules
|
12 |
+
|
13 |
+
Core package dependencies in the monorepo are defined using "workspace:*"
|
14 |
+
|
15 |
+
But to release them as NPM modules, you need to replace "workspace:*" by the actual version number
|
16 |
+
|
package.json
CHANGED
@@ -12,10 +12,10 @@
|
|
12 |
"start": "bun run --cwd packages/app start",
|
13 |
"start:prod": "bun run --cwd packages/app start:prod",
|
14 |
"build": "bun run build:all",
|
15 |
-
"build:all": "bun run build:clap && bun run build:timeline && bun run build:
|
16 |
"build:clap": "bun run --cwd packages/clap build",
|
17 |
"build:timeline": "bun run --cwd packages/timeline build",
|
18 |
-
"build:
|
19 |
"build:io": "bun run --cwd packages/io build",
|
20 |
"build:colors": "bun run --cwd packages/colors build",
|
21 |
"build:engine": "bun run --cwd packages/engine build",
|
@@ -23,7 +23,7 @@
|
|
23 |
"build:clapper-services": "bun run --cwd packages/clapper-services build",
|
24 |
"build:app": "bun run --cwd packages/app build",
|
25 |
"test": "bun run test:all",
|
26 |
-
"test:all": "bun run --cwd packages/clap test && bun run --cwd packages/timeline test && bun run --cwd packages/
|
27 |
"format": "bun run --cwd packages/app format"
|
28 |
},
|
29 |
"trustedDependencies": [
|
@@ -34,7 +34,7 @@
|
|
34 |
"workspaces": [
|
35 |
"packages/clap",
|
36 |
"packages/timeline",
|
37 |
-
"packages/
|
38 |
"packages/io",
|
39 |
"packages/colors",
|
40 |
"packages/engine",
|
|
|
12 |
"start": "bun run --cwd packages/app start",
|
13 |
"start:prod": "bun run --cwd packages/app start:prod",
|
14 |
"build": "bun run build:all",
|
15 |
+
"build:all": "bun run build:clap && bun run build:timeline && bun run build:client && bun run build:io && bun run build:colors && bun run build:engine && bun run build:broadway && bun run build:clapper-services && bun run build:app",
|
16 |
"build:clap": "bun run --cwd packages/clap build",
|
17 |
"build:timeline": "bun run --cwd packages/timeline build",
|
18 |
+
"build:client": "bun run --cwd packages/client build",
|
19 |
"build:io": "bun run --cwd packages/io build",
|
20 |
"build:colors": "bun run --cwd packages/colors build",
|
21 |
"build:engine": "bun run --cwd packages/engine build",
|
|
|
23 |
"build:clapper-services": "bun run --cwd packages/clapper-services build",
|
24 |
"build:app": "bun run --cwd packages/app build",
|
25 |
"test": "bun run test:all",
|
26 |
+
"test:all": "bun run --cwd packages/clap test && bun run --cwd packages/timeline test && bun run --cwd packages/client test && bun run --cwd packages/io test && bun run --cwd packages/colors test && bun run --cwd packages/engine test && bun run --cwd packages/broadway test && bun run --cwd packages/clapper-services test && bun run --cwd packages/app test",
|
27 |
"format": "bun run --cwd packages/app format"
|
28 |
},
|
29 |
"trustedDependencies": [
|
|
|
34 |
"workspaces": [
|
35 |
"packages/clap",
|
36 |
"packages/timeline",
|
37 |
+
"packages/client",
|
38 |
"packages/io",
|
39 |
"packages/colors",
|
40 |
"packages/engine",
|
packages/api-client/.gitignore
DELETED
@@ -1,177 +0,0 @@
|
|
1 |
-
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
2 |
-
|
3 |
-
# Logs
|
4 |
-
|
5 |
-
logs
|
6 |
-
_.log
|
7 |
-
npm-debug.log_
|
8 |
-
yarn-debug.log*
|
9 |
-
yarn-error.log*
|
10 |
-
lerna-debug.log*
|
11 |
-
.pnpm-debug.log*
|
12 |
-
|
13 |
-
# Diagnostic reports (https://nodejs.org/api/report.html)
|
14 |
-
|
15 |
-
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
16 |
-
|
17 |
-
# Runtime data
|
18 |
-
|
19 |
-
pids
|
20 |
-
_.pid
|
21 |
-
_.seed
|
22 |
-
\*.pid.lock
|
23 |
-
|
24 |
-
# Directory for instrumented libs generated by jscoverage/JSCover
|
25 |
-
|
26 |
-
lib-cov
|
27 |
-
|
28 |
-
# Coverage directory used by tools like istanbul
|
29 |
-
|
30 |
-
coverage
|
31 |
-
\*.lcov
|
32 |
-
|
33 |
-
# nyc test coverage
|
34 |
-
|
35 |
-
.nyc_output
|
36 |
-
|
37 |
-
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
38 |
-
|
39 |
-
.grunt
|
40 |
-
|
41 |
-
# Bower dependency directory (https://bower.io/)
|
42 |
-
|
43 |
-
bower_components
|
44 |
-
|
45 |
-
# node-waf configuration
|
46 |
-
|
47 |
-
.lock-wscript
|
48 |
-
|
49 |
-
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
50 |
-
|
51 |
-
build/Release
|
52 |
-
|
53 |
-
# Dependency directories
|
54 |
-
|
55 |
-
node_modules/
|
56 |
-
jspm_packages/
|
57 |
-
|
58 |
-
# Snowpack dependency directory (https://snowpack.dev/)
|
59 |
-
|
60 |
-
web_modules/
|
61 |
-
|
62 |
-
# TypeScript cache
|
63 |
-
|
64 |
-
\*.tsbuildinfo
|
65 |
-
|
66 |
-
# Optional npm cache directory
|
67 |
-
|
68 |
-
.npm
|
69 |
-
|
70 |
-
# Optional eslint cache
|
71 |
-
|
72 |
-
.eslintcache
|
73 |
-
|
74 |
-
# Optional stylelint cache
|
75 |
-
|
76 |
-
.stylelintcache
|
77 |
-
|
78 |
-
# Microbundle cache
|
79 |
-
|
80 |
-
.rpt2_cache/
|
81 |
-
.rts2_cache_cjs/
|
82 |
-
.rts2_cache_es/
|
83 |
-
.rts2_cache_umd/
|
84 |
-
|
85 |
-
# Optional REPL history
|
86 |
-
|
87 |
-
.node_repl_history
|
88 |
-
|
89 |
-
# Output of 'npm pack'
|
90 |
-
|
91 |
-
\*.tgz
|
92 |
-
|
93 |
-
# Yarn Integrity file
|
94 |
-
|
95 |
-
.yarn-integrity
|
96 |
-
|
97 |
-
# dotenv environment variable files
|
98 |
-
|
99 |
-
.env
|
100 |
-
.env.development.local
|
101 |
-
.env.test.local
|
102 |
-
.env.production.local
|
103 |
-
.env.local
|
104 |
-
|
105 |
-
# parcel-bundler cache (https://parceljs.org/)
|
106 |
-
|
107 |
-
.cache
|
108 |
-
.parcel-cache
|
109 |
-
|
110 |
-
# Next.js build output
|
111 |
-
|
112 |
-
.next
|
113 |
-
out
|
114 |
-
|
115 |
-
# Nuxt.js build / generate output
|
116 |
-
dist
|
117 |
-
.nuxt
|
118 |
-
|
119 |
-
# Gatsby files
|
120 |
-
|
121 |
-
.cache/
|
122 |
-
|
123 |
-
# Comment in the public line in if your project uses Gatsby and not Next.js
|
124 |
-
|
125 |
-
# https://nextjs.org/blog/next-9-1#public-directory-support
|
126 |
-
|
127 |
-
# public
|
128 |
-
|
129 |
-
# vuepress build output
|
130 |
-
|
131 |
-
.vuepress/dist
|
132 |
-
|
133 |
-
# vuepress v2.x temp and cache directory
|
134 |
-
|
135 |
-
.temp
|
136 |
-
.cache
|
137 |
-
|
138 |
-
# Docusaurus cache and generated files
|
139 |
-
|
140 |
-
.docusaurus
|
141 |
-
|
142 |
-
# Serverless directories
|
143 |
-
|
144 |
-
.serverless/
|
145 |
-
|
146 |
-
# FuseBox cache
|
147 |
-
|
148 |
-
.fusebox/
|
149 |
-
|
150 |
-
# DynamoDB Local files
|
151 |
-
|
152 |
-
.dynamodb/
|
153 |
-
|
154 |
-
# TernJS port file
|
155 |
-
|
156 |
-
.tern-port
|
157 |
-
|
158 |
-
# Stores VSCode versions used for testing VSCode extensions
|
159 |
-
|
160 |
-
.vscode-test
|
161 |
-
|
162 |
-
# yarn v2
|
163 |
-
|
164 |
-
.yarn/cache
|
165 |
-
.yarn/unplugged
|
166 |
-
.yarn/build-state.yml
|
167 |
-
.yarn/install-state.gz
|
168 |
-
.pnp.\*
|
169 |
-
|
170 |
-
# IntelliJ based IDEs
|
171 |
-
.idea
|
172 |
-
|
173 |
-
# Finder (MacOS) folder config
|
174 |
-
.DS_Store
|
175 |
-
|
176 |
-
# TypeScript build information
|
177 |
-
*.tsbuildinfo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/.npmignore
DELETED
@@ -1,4 +0,0 @@
|
|
1 |
-
# Ignore everything
|
2 |
-
*
|
3 |
-
# Except the dist directory
|
4 |
-
!dist/
|
|
|
|
|
|
|
|
|
|
packages/api-client/.prettierrc.json
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"semi": false,
|
3 |
-
"singleQuote": true,
|
4 |
-
"arrowParens": "avoid",
|
5 |
-
"printWidth": 140,
|
6 |
-
"tabWidth": 2,
|
7 |
-
"trailingComma": "es5",
|
8 |
-
"bracketSpacing": true
|
9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/LICENSE.md
DELETED
@@ -1,21 +0,0 @@
|
|
1 |
-
# MIT License
|
2 |
-
|
3 |
-
Copyright (c) 2024 Julian Bilcke
|
4 |
-
|
5 |
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
-
of this software and associated documentation files (the "Software"), to deal
|
7 |
-
in the Software without restriction, including without limitation the rights
|
8 |
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
-
copies of the Software, and to permit persons to whom the Software is
|
10 |
-
furnished to do so, subject to the following conditions:
|
11 |
-
|
12 |
-
The above copyright notice and this permission notice shall be included in all
|
13 |
-
copies or substantial portions of the Software.
|
14 |
-
|
15 |
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
-
SOFTWARE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/README.md
DELETED
@@ -1,113 +0,0 @@
|
|
1 |
-
# @aitube/api-client
|
2 |
-
|
3 |
-
*Official API client for AiTube.at*
|
4 |
-
|
5 |
-
## ATTENTION
|
6 |
-
|
7 |
-
AiTube is currently in heavy development, and for the moment
|
8 |
-
the API client is reserved for *private use* (it is used by AI Stories Factory).
|
9 |
-
|
10 |
-
We are sorry for any inconvenience this might cause.
|
11 |
-
|
12 |
-
## Caveats
|
13 |
-
|
14 |
-
The official domain for AiTube is `aitube.at`, however right now
|
15 |
-
the Hugging Face Space is not configured to use this as a domain,
|
16 |
-
so we need to perform all API calls to `jbilcke-hf-ai-tube.hf.space`.
|
17 |
-
|
18 |
-
## Installation
|
19 |
-
|
20 |
-
To install the package, run the following command:
|
21 |
-
|
22 |
-
```bash
|
23 |
-
npm install @aitube/api-client
|
24 |
-
```
|
25 |
-
|
26 |
-
## Getting Started
|
27 |
-
|
28 |
-
Note: to overridethe AiTube API URL, set this env var: `AITUBE_URL`
|
29 |
-
|
30 |
-
```typescript
|
31 |
-
import {
|
32 |
-
createClap,
|
33 |
-
editClapDialogues,
|
34 |
-
editClapEntities,
|
35 |
-
editClapMusic,
|
36 |
-
editClapSounds,
|
37 |
-
editClapStory,
|
38 |
-
editClapStoryboards,
|
39 |
-
editClapVideos,
|
40 |
-
exportClapToVideo,
|
41 |
-
defaultAitubeHostname,
|
42 |
-
defaultClapWidth,
|
43 |
-
defaultClapHeight,
|
44 |
-
defaultExportFormat,
|
45 |
-
aitubeUrl,
|
46 |
-
aitubeApiVersion,
|
47 |
-
aitubeApiUrl,
|
48 |
-
ClapEntityPrompt,
|
49 |
-
SupportedExportFormat,
|
50 |
-
applyClapCompletion,
|
51 |
-
} from '@aitube/api-client'
|
52 |
-
|
53 |
-
const ultraSecret = "ultra secret token unavailable to common mortals"
|
54 |
-
|
55 |
-
const basicClap = await createClap({
|
56 |
-
prompt: "story about a dog",
|
57 |
-
turbo: false,
|
58 |
-
token: ultraSecret
|
59 |
-
})
|
60 |
-
|
61 |
-
const illustratedClap = await editClapStoryboards({
|
62 |
-
clap: basicClap,
|
63 |
-
turbo: false,
|
64 |
-
token: ultraSecret
|
65 |
-
})
|
66 |
-
|
67 |
-
const mp4VideoFile = await exportClapToVideo({
|
68 |
-
clap: illustratedClap,
|
69 |
-
format: "mp4",
|
70 |
-
turbo: false,
|
71 |
-
token: ultraSecret
|
72 |
-
})
|
73 |
-
```
|
74 |
-
|
75 |
-
## Customizing the server
|
76 |
-
|
77 |
-
The hostname can be overriden by defining the `AITUBE_URL` environment variable.
|
78 |
-
|
79 |
-
eg:
|
80 |
-
|
81 |
-
```bash
|
82 |
-
AITUBE_URL=http://localhost:3000
|
83 |
-
```
|
84 |
-
|
85 |
-
## Build Instructions
|
86 |
-
|
87 |
-
Install [Bun](https://bun.sh/)
|
88 |
-
|
89 |
-
Run the following commands:
|
90 |
-
|
91 |
-
```bash
|
92 |
-
bun install
|
93 |
-
|
94 |
-
bun run build
|
95 |
-
```
|
96 |
-
|
97 |
-
To publish:
|
98 |
-
|
99 |
-
```bash
|
100 |
-
bun run build
|
101 |
-
|
102 |
-
bun run build:declaration
|
103 |
-
|
104 |
-
bun run publish
|
105 |
-
```
|
106 |
-
|
107 |
-
## Contributing
|
108 |
-
|
109 |
-
We welcome contributions! Please feel free to submit a pull request.
|
110 |
-
|
111 |
-
## License
|
112 |
-
|
113 |
-
This package is under the MIT License. See `LICENSE` file for more details.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/package.json
DELETED
@@ -1,46 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"name": "@aitube/api-client",
|
3 |
-
"module": "index.ts",
|
4 |
-
"main": "dist/index.js",
|
5 |
-
"types": "dist/index.d.ts",
|
6 |
-
"type": "module",
|
7 |
-
"version": "0.2.4",
|
8 |
-
"description": "Official API client for AiTube.at",
|
9 |
-
"scripts": {
|
10 |
-
"build": "bun build ./src/index.ts --outfile=dist/index.js --external=@aitube/clap && bun run build:declaration",
|
11 |
-
"build:declaration": "tsc --emitDeclarationOnly --project tsconfig.types.json",
|
12 |
-
"postbuild": "rimraf tsconfig.types.tsbuildinfo && bun run build:declaration",
|
13 |
-
"publish": "npm publish --access public",
|
14 |
-
"update": "rm -Rf node_modules && rm bun.lockb && bun i && bun run build"
|
15 |
-
},
|
16 |
-
"devDependencies": {
|
17 |
-
"bun-types": "latest",
|
18 |
-
"prettier": "^3.3.3",
|
19 |
-
"rimraf": "^6.0.1",
|
20 |
-
"typescript": "^5.5.4"
|
21 |
-
},
|
22 |
-
"repository": {
|
23 |
-
"type": "git",
|
24 |
-
"url": "https://github.com/jbilcke-hf/aitube-client.git"
|
25 |
-
},
|
26 |
-
"keywords": [
|
27 |
-
"Clapper.app",
|
28 |
-
"AiTube.at",
|
29 |
-
"OpenClap",
|
30 |
-
"AI cinema",
|
31 |
-
"file format",
|
32 |
-
"specification"
|
33 |
-
],
|
34 |
-
"author": "Julian Bilcke",
|
35 |
-
"license": "MIT",
|
36 |
-
"files": [
|
37 |
-
"dist/*.js",
|
38 |
-
"dist/*.d.ts",
|
39 |
-
"dist/**/*.d.ts"
|
40 |
-
],
|
41 |
-
"dependencies": {
|
42 |
-
"@aitube/clap": "workspace:*",
|
43 |
-
"@types/bun": "latest",
|
44 |
-
"query-string": "^9.0.0"
|
45 |
-
}
|
46 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/createClap.ts
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
import { ClapProject, fetchClap } from "@aitube/clap"
|
2 |
-
|
3 |
-
import { aitubeApiUrl } from "@/constants/config"
|
4 |
-
|
5 |
-
import { defaultClapHeight, defaultClapWidth } from "@/constants/defaultValues"
|
6 |
-
|
7 |
-
export async function createClap({
|
8 |
-
prompt,
|
9 |
-
height = defaultClapHeight,
|
10 |
-
width = defaultClapWidth,
|
11 |
-
turbo = false,
|
12 |
-
token,
|
13 |
-
}: {
|
14 |
-
prompt: string
|
15 |
-
height?: number
|
16 |
-
width?: number
|
17 |
-
turbo?: boolean
|
18 |
-
token?: string
|
19 |
-
}): Promise<ClapProject> {
|
20 |
-
|
21 |
-
if (typeof prompt !== "string" || !prompt.length) { throw new Error(`please provide a prompt`) }
|
22 |
-
|
23 |
-
const hasToken = typeof token === "string" && token.length > 0
|
24 |
-
|
25 |
-
const clap = await fetchClap(`${aitubeApiUrl}create`, {
|
26 |
-
method: "POST",
|
27 |
-
headers: {
|
28 |
-
"Content-Type": "application/json",
|
29 |
-
...hasToken && {
|
30 |
-
"Authorization": `Bearer ${token}`
|
31 |
-
}
|
32 |
-
},
|
33 |
-
body: JSON.stringify({
|
34 |
-
prompt,
|
35 |
-
width,
|
36 |
-
height,
|
37 |
-
turbo,
|
38 |
-
}),
|
39 |
-
cache: "no-store",
|
40 |
-
})
|
41 |
-
|
42 |
-
return clap
|
43 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/editClapDialogues.ts
DELETED
@@ -1,61 +0,0 @@
|
|
1 |
-
import { ClapCompletionMode, ClapProject, fetchClap, removeGeneratedAssetUrls, serializeClap } from "@aitube/clap"
|
2 |
-
import queryString from "query-string"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { applyClapCompletion } from "@/utils"
|
6 |
-
|
7 |
-
export async function editClapDialogues({
|
8 |
-
clap,
|
9 |
-
completionMode = ClapCompletionMode.MERGE,
|
10 |
-
turbo = false,
|
11 |
-
token,
|
12 |
-
}: {
|
13 |
-
clap: ClapProject
|
14 |
-
|
15 |
-
/**
|
16 |
-
* Completion mode (optional, defaults to "merge")
|
17 |
-
*
|
18 |
-
* Possible values are:
|
19 |
-
* - full: the API and the client will return a full clap file. This is a very convenient and simple mode, but it is also very ineficient, so it should not be used for intensive applications.
|
20 |
-
* - partial: the API and the client will return a partial clap file, containing only the new values and changes. This is useful for real-time applications and streaming.
|
21 |
-
* - merge: the API will return a partial clap file, and the client will return a merge of the original with the new values. This is safe to run, there are no side-effects.
|
22 |
-
* - replace: the API will return a partial clap file, and the client will replace the original. This is the most efficient mode, but it relies on side-effects and inline object updates.
|
23 |
-
*/
|
24 |
-
completionMode?: ClapCompletionMode
|
25 |
-
|
26 |
-
turbo?: boolean
|
27 |
-
|
28 |
-
token?: string
|
29 |
-
}): Promise<ClapProject> {
|
30 |
-
|
31 |
-
if (!clap) { throw new Error(`please provide a valid clap project`) }
|
32 |
-
|
33 |
-
const hasToken = typeof token === "string" && token.length > 0
|
34 |
-
|
35 |
-
const params: Record<string, any> = {}
|
36 |
-
|
37 |
-
if (typeof completionMode === "string") {
|
38 |
-
params.c = completionMode
|
39 |
-
}
|
40 |
-
|
41 |
-
if (turbo) {
|
42 |
-
params.t = "true"
|
43 |
-
}
|
44 |
-
|
45 |
-
const newClap = await fetchClap(
|
46 |
-
`${aitubeApiUrl}edit/dialogues?${queryString.stringify(params)}`, {
|
47 |
-
method: "POST",
|
48 |
-
headers: {
|
49 |
-
"Content-Type": "application/x-gzip",
|
50 |
-
...hasToken && {
|
51 |
-
"Authorization": `Bearer ${token}`
|
52 |
-
}
|
53 |
-
},
|
54 |
-
body: await serializeClap(removeGeneratedAssetUrls(clap)),
|
55 |
-
cache: "no-store",
|
56 |
-
})
|
57 |
-
|
58 |
-
const result = await applyClapCompletion(clap, newClap, completionMode)
|
59 |
-
|
60 |
-
return result
|
61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/editClapEntities.ts
DELETED
@@ -1,75 +0,0 @@
|
|
1 |
-
import { ClapCompletionMode, ClapProject, fetchClap, serializeClap, removeGeneratedAssetUrls } from "@aitube/clap"
|
2 |
-
import queryString from "query-string"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { ClapEntityPrompt } from "@/constants/types"
|
6 |
-
import { applyClapCompletion } from "@/utils"
|
7 |
-
|
8 |
-
export async function editClapEntities({
|
9 |
-
clap,
|
10 |
-
entityPrompts = [],
|
11 |
-
completionMode = ClapCompletionMode.MERGE,
|
12 |
-
turbo = false,
|
13 |
-
token,
|
14 |
-
}: {
|
15 |
-
// A ClapProject instance
|
16 |
-
clap: ClapProject
|
17 |
-
|
18 |
-
// a list of entity prompts
|
19 |
-
entityPrompts?: ClapEntityPrompt[],
|
20 |
-
|
21 |
-
/**
|
22 |
-
* Completion mode (optional, defaults to "merge")
|
23 |
-
*
|
24 |
-
* Possible values are:
|
25 |
-
* - full: the API and the client will return a full clap file. This is a very convenient and simple mode, but it is also very ineficient, so it should not be used for intensive applications.
|
26 |
-
* - partial: the API and the client will return a partial clap file, containing only the new values and changes. This is useful for real-time applications and streaming.
|
27 |
-
* - merge: the API will return a partial clap file, and the client will return a merge of the original with the new values. This is safe to run, there are no side-effects.
|
28 |
-
* - replace: the API will return a partial clap file, and the client will replace the original. This is the most efficient mode, but it relies on side-effects and inline object updates.
|
29 |
-
*/
|
30 |
-
completionMode?: ClapCompletionMode
|
31 |
-
|
32 |
-
turbo?: boolean
|
33 |
-
|
34 |
-
token?: string
|
35 |
-
}): Promise<ClapProject> {
|
36 |
-
|
37 |
-
if (!clap) { throw new Error(`please provide a clap to extend`) }
|
38 |
-
|
39 |
-
const hasToken = typeof token === "string" && token.length > 0
|
40 |
-
|
41 |
-
const params: Record<string, any> = {}
|
42 |
-
|
43 |
-
if (typeof completionMode === "string") {
|
44 |
-
params.c = completionMode
|
45 |
-
}
|
46 |
-
|
47 |
-
if (turbo) {
|
48 |
-
params.t = "true"
|
49 |
-
}
|
50 |
-
|
51 |
-
if (entityPrompts.length) {
|
52 |
-
// if "params.e = JSON.stringify(item)" works with UTF-8 characters,
|
53 |
-
// then we don't need to import "js-base64"
|
54 |
-
// otherwise you will have to do:
|
55 |
-
// params.e = jsBase64.encode(JSON.stringify(item))
|
56 |
-
params.e = JSON.stringify(entityPrompts)
|
57 |
-
}
|
58 |
-
|
59 |
-
const newClap = await fetchClap(
|
60 |
-
`${aitubeApiUrl}edit/entities?${queryString.stringify(params)}`, {
|
61 |
-
method: "POST",
|
62 |
-
headers: {
|
63 |
-
"Content-Type": "application/x-gzip",
|
64 |
-
...hasToken && {
|
65 |
-
"Authorization": `Bearer ${token}`
|
66 |
-
}
|
67 |
-
},
|
68 |
-
body: await serializeClap(removeGeneratedAssetUrls(clap)),
|
69 |
-
cache: "no-store",
|
70 |
-
})
|
71 |
-
|
72 |
-
const result = await applyClapCompletion(clap, newClap, completionMode)
|
73 |
-
|
74 |
-
return result
|
75 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/editClapMusic.ts
DELETED
@@ -1,60 +0,0 @@
|
|
1 |
-
import queryString from "query-string"
|
2 |
-
import { ClapCompletionMode, ClapProject, fetchClap, serializeClap, removeGeneratedAssetUrls } from "@aitube/clap"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { applyClapCompletion } from "@/utils"
|
6 |
-
|
7 |
-
export async function editClapMusic({
|
8 |
-
clap,
|
9 |
-
completionMode = ClapCompletionMode.MERGE,
|
10 |
-
turbo = false,
|
11 |
-
token,
|
12 |
-
}: {
|
13 |
-
clap: ClapProject
|
14 |
-
|
15 |
-
/**
|
16 |
-
* Completion mode (optional, defaults to "merge")
|
17 |
-
*
|
18 |
-
* Possible values are:
|
19 |
-
* - full: the API and the client will return a full clap file. This is a very convenient and simple mode, but it is also very ineficient, so it should not be used for intensive applications.
|
20 |
-
* - partial: the API and the client will return a partial clap file, containing only the new values and changes. This is useful for real-time applications and streaming.
|
21 |
-
* - merge: the API will return a partial clap file, and the client will return a merge of the original with the new values. This is safe to run, there are no side-effects.
|
22 |
-
* - replace: the API will return a partial clap file, and the client will replace the original. This is the most efficient mode, but it relies on side-effects and inline object updates.
|
23 |
-
*/
|
24 |
-
completionMode?: ClapCompletionMode
|
25 |
-
|
26 |
-
turbo?: boolean
|
27 |
-
|
28 |
-
token?: string
|
29 |
-
}): Promise<ClapProject> {
|
30 |
-
|
31 |
-
if (!clap) { throw new Error(`please provide a valid clap project`) }
|
32 |
-
|
33 |
-
const hasToken = typeof token === "string" && token.length > 0
|
34 |
-
|
35 |
-
const params: Record<string, any> = {}
|
36 |
-
|
37 |
-
if (typeof completionMode === "string") {
|
38 |
-
params.c = completionMode
|
39 |
-
}
|
40 |
-
|
41 |
-
if (turbo) {
|
42 |
-
params.t = "true"
|
43 |
-
}
|
44 |
-
|
45 |
-
const newClap = await fetchClap(`${aitubeApiUrl}edit/music?${queryString.stringify(params)}`, {
|
46 |
-
method: "POST",
|
47 |
-
headers: {
|
48 |
-
"Content-Type": "application/x-gzip",
|
49 |
-
...hasToken && {
|
50 |
-
"Authorization": `Bearer ${token}`
|
51 |
-
}
|
52 |
-
},
|
53 |
-
body: await serializeClap(removeGeneratedAssetUrls(clap)),
|
54 |
-
cache: "no-store",
|
55 |
-
})
|
56 |
-
|
57 |
-
const result = await applyClapCompletion(clap, newClap, completionMode)
|
58 |
-
|
59 |
-
return result
|
60 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/editClapSounds.ts
DELETED
@@ -1,60 +0,0 @@
|
|
1 |
-
import queryString from "query-string"
|
2 |
-
import { ClapCompletionMode, ClapProject, fetchClap, serializeClap, removeGeneratedAssetUrls } from "@aitube/clap"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { applyClapCompletion } from "@/utils"
|
6 |
-
|
7 |
-
export async function editClapSounds({
|
8 |
-
clap,
|
9 |
-
completionMode = ClapCompletionMode.MERGE,
|
10 |
-
turbo = false,
|
11 |
-
token,
|
12 |
-
}: {
|
13 |
-
clap: ClapProject
|
14 |
-
|
15 |
-
/**
|
16 |
-
* Completion mode (optional, defaults to "merge")
|
17 |
-
*
|
18 |
-
* Possible values are:
|
19 |
-
* - full: the API and the client will return a full clap file. This is a very convenient and simple mode, but it is also very ineficient, so it should not be used for intensive applications.
|
20 |
-
* - partial: the API and the client will return a partial clap file, containing only the new values and changes. This is useful for real-time applications and streaming.
|
21 |
-
* - merge: the API will return a partial clap file, and the client will return a merge of the original with the new values. This is safe to run, there are no side-effects.
|
22 |
-
* - replace: the API will return a partial clap file, and the client will replace the original. This is the most efficient mode, but it relies on side-effects and inline object updates.
|
23 |
-
*/
|
24 |
-
completionMode?: ClapCompletionMode
|
25 |
-
|
26 |
-
turbo?: boolean
|
27 |
-
|
28 |
-
token?: string
|
29 |
-
}): Promise<ClapProject> {
|
30 |
-
|
31 |
-
if (!clap) { throw new Error(`please provide a valid clap project`) }
|
32 |
-
|
33 |
-
const hasToken = typeof token === "string" && token.length > 0
|
34 |
-
|
35 |
-
const params: Record<string, any> = {}
|
36 |
-
|
37 |
-
if (typeof completionMode === "string") {
|
38 |
-
params.c = completionMode
|
39 |
-
}
|
40 |
-
|
41 |
-
if (turbo) {
|
42 |
-
params.t = "true"
|
43 |
-
}
|
44 |
-
|
45 |
-
const newClap = await fetchClap(`${aitubeApiUrl}edit/sounds?${queryString.stringify(params)}`, {
|
46 |
-
method: "POST",
|
47 |
-
headers: {
|
48 |
-
"Content-Type": "application/x-gzip",
|
49 |
-
...hasToken && {
|
50 |
-
"Authorization": `Bearer ${token}`
|
51 |
-
}
|
52 |
-
},
|
53 |
-
body: await serializeClap(removeGeneratedAssetUrls(clap)),
|
54 |
-
cache: "no-store",
|
55 |
-
})
|
56 |
-
|
57 |
-
const result = await applyClapCompletion(clap, newClap, completionMode)
|
58 |
-
|
59 |
-
return result
|
60 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/editClapStory.ts
DELETED
@@ -1,114 +0,0 @@
|
|
1 |
-
import { ClapCompletionMode, ClapProject, fetchClap, filterAssets, isValidNumber, serializeClap, removeGeneratedAssetUrls } from "@aitube/clap"
|
2 |
-
import queryString from "query-string"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { applyClapCompletion } from "@/utils"
|
6 |
-
|
7 |
-
export async function editClapStory({
|
8 |
-
clap,
|
9 |
-
prompt,
|
10 |
-
startTimeInMs,
|
11 |
-
endTimeInMs,
|
12 |
-
completionMode = ClapCompletionMode.MERGE,
|
13 |
-
turbo = false,
|
14 |
-
token,
|
15 |
-
}: {
|
16 |
-
// A ClapProject instance
|
17 |
-
clap: ClapProject
|
18 |
-
|
19 |
-
// a prompt to describe how to extend the story (optional)
|
20 |
-
prompt?: string
|
21 |
-
|
22 |
-
// indicates where the completion should start in the timeline
|
23 |
-
//
|
24 |
-
// this can be used tp jump to arbitrary timestamps
|
25 |
-
// in the story
|
26 |
-
//
|
27 |
-
// if you pick a start time AFTER the current project's end time,
|
28 |
-
// the project will be extended
|
29 |
-
//
|
30 |
-
// default value: the current project's end time
|
31 |
-
startTimeInMs?: number
|
32 |
-
|
33 |
-
// it is recommended to use a
|
34 |
-
// end time (eg. startTimeInMs + 12000)
|
35 |
-
// if left by default, th server will generate
|
36 |
-
//
|
37 |
-
// for performance and security reasons,
|
38 |
-
// the server may enforce a hardcoded limit to bypass what you
|
39 |
-
// set here, but that is not a big deal because you can pass
|
40 |
-
// you .clap file again if necessary
|
41 |
-
//
|
42 |
-
// default value: no limit (the server will set one)
|
43 |
-
endTimeInMs?: number
|
44 |
-
|
45 |
-
/**
|
46 |
-
* Completion mode (optional, defaults to "merge")
|
47 |
-
*
|
48 |
-
* Possible values are:
|
49 |
-
* - full: the API and the client will return a full clap file. This is a very convenient and simple mode, but it is also very ineficient, so it should not be used for intensive applications.
|
50 |
-
* - partial: the API and the client will return a partial clap file, containing only the new values and changes. This is useful for real-time applications and streaming.
|
51 |
-
* - merge: the API will return a partial clap file, and the client will return a merge of the original with the new values. This is safe to run, there are no side-effects.
|
52 |
-
* - replace: the API will return a partial clap file, and the client will replace the original. This is the most efficient mode, but it relies on side-effects and inline object updates.
|
53 |
-
*/
|
54 |
-
completionMode?: ClapCompletionMode
|
55 |
-
|
56 |
-
turbo?: boolean
|
57 |
-
|
58 |
-
token?: string
|
59 |
-
}): Promise<ClapProject> {
|
60 |
-
|
61 |
-
if (!clap) { throw new Error(`please provide a clap to extend`) }
|
62 |
-
|
63 |
-
const hasToken = typeof token === "string" && token.length > 0
|
64 |
-
|
65 |
-
const params: Record<string, any> = {}
|
66 |
-
|
67 |
-
if (typeof completionMode === "string") {
|
68 |
-
params.c = completionMode
|
69 |
-
}
|
70 |
-
|
71 |
-
if (typeof prompt === "string" && prompt.length > 0) {
|
72 |
-
params.p = prompt
|
73 |
-
}
|
74 |
-
|
75 |
-
if (isValidNumber(startTimeInMs)) {
|
76 |
-
params.s = startTimeInMs
|
77 |
-
}
|
78 |
-
|
79 |
-
if (isValidNumber(endTimeInMs)) {
|
80 |
-
params.e = endTimeInMs
|
81 |
-
}
|
82 |
-
|
83 |
-
if (turbo) {
|
84 |
-
params.t = "true"
|
85 |
-
}
|
86 |
-
|
87 |
-
// we remove heavy elements from the payload
|
88 |
-
const payload = await filterAssets({
|
89 |
-
clap,
|
90 |
-
mode: "INCLUDE",
|
91 |
-
categories: {},
|
92 |
-
immutable: true, // to create a standalone copy
|
93 |
-
|
94 |
-
// we only remove the data, but we still keep things marked as "generated"
|
95 |
-
updateStatus: false,
|
96 |
-
})
|
97 |
-
|
98 |
-
const newClap = await fetchClap(
|
99 |
-
`${aitubeApiUrl}edit/story?${queryString.stringify(params)}`, {
|
100 |
-
method: "POST",
|
101 |
-
headers: {
|
102 |
-
"Content-Type": "application/x-gzip",
|
103 |
-
...hasToken && {
|
104 |
-
"Authorization": `Bearer ${token}`
|
105 |
-
}
|
106 |
-
},
|
107 |
-
body: await serializeClap(removeGeneratedAssetUrls(clap)),
|
108 |
-
cache: "no-store",
|
109 |
-
})
|
110 |
-
|
111 |
-
const result = await applyClapCompletion(clap, newClap, completionMode)
|
112 |
-
|
113 |
-
return result
|
114 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/editClapStoryboards.ts
DELETED
@@ -1,60 +0,0 @@
|
|
1 |
-
import queryString from "query-string"
|
2 |
-
import { ClapCompletionMode, ClapProject, fetchClap, serializeClap, removeGeneratedAssetUrls } from "@aitube/clap"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { applyClapCompletion } from "@/utils"
|
6 |
-
|
7 |
-
export async function editClapStoryboards({
|
8 |
-
clap,
|
9 |
-
completionMode = ClapCompletionMode.MERGE,
|
10 |
-
turbo = false,
|
11 |
-
token,
|
12 |
-
}: {
|
13 |
-
clap: ClapProject
|
14 |
-
|
15 |
-
/**
|
16 |
-
* Completion mode (optional, defaults to "merge")
|
17 |
-
*
|
18 |
-
* Possible values are:
|
19 |
-
* - full: the API and the client will return a full clap file. This is a very convenient and simple mode, but it is also very ineficient, so it should not be used for intensive applications.
|
20 |
-
* - partial: the API and the client will return a partial clap file, containing only the new values and changes. This is useful for real-time applications and streaming.
|
21 |
-
* - merge: the API will return a partial clap file, and the client will return a merge of the original with the new values. This is safe to run, there are no side-effects.
|
22 |
-
* - replace: the API will return a partial clap file, and the client will replace the original. This is the most efficient mode, but it relies on side-effects and inline object updates.
|
23 |
-
*/
|
24 |
-
completionMode?: ClapCompletionMode
|
25 |
-
|
26 |
-
turbo?: boolean
|
27 |
-
|
28 |
-
token?: string
|
29 |
-
}): Promise<ClapProject> {
|
30 |
-
|
31 |
-
if (!clap) { throw new Error(`please provide a valid clap project`) }
|
32 |
-
|
33 |
-
const hasToken = typeof token === "string" && token.length > 0
|
34 |
-
|
35 |
-
const params: Record<string, any> = {}
|
36 |
-
|
37 |
-
if (typeof completionMode === "string") {
|
38 |
-
params.c = completionMode
|
39 |
-
}
|
40 |
-
|
41 |
-
if (turbo) {
|
42 |
-
params.t = "true"
|
43 |
-
}
|
44 |
-
|
45 |
-
const newClap = await fetchClap(`${aitubeApiUrl}edit/storyboards?${queryString.stringify(params)}`, {
|
46 |
-
method: "POST",
|
47 |
-
headers: {
|
48 |
-
"Content-Type": "application/x-gzip",
|
49 |
-
...hasToken && {
|
50 |
-
"Authorization": `Bearer ${token}`
|
51 |
-
}
|
52 |
-
},
|
53 |
-
body: await serializeClap(removeGeneratedAssetUrls(clap)),
|
54 |
-
cache: "no-store",
|
55 |
-
})
|
56 |
-
|
57 |
-
const result = await applyClapCompletion(clap, newClap, completionMode)
|
58 |
-
|
59 |
-
return result
|
60 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/editClapVideos.ts
DELETED
@@ -1,85 +0,0 @@
|
|
1 |
-
import queryString from "query-string"
|
2 |
-
import { ClapCompletionMode, ClapProject, fetchClap, serializeClap, removeGeneratedAssetUrls, ClapSegmentStatus, ClapSegmentCategory, filterSegments, ClapSegmentFilteringMode, ClapSegment } from "@aitube/clap"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { applyClapCompletion } from "@/utils"
|
6 |
-
|
7 |
-
export async function editClapVideos({
|
8 |
-
clap,
|
9 |
-
completionMode = ClapCompletionMode.MERGE,
|
10 |
-
turbo = false,
|
11 |
-
token,
|
12 |
-
}: {
|
13 |
-
clap: ClapProject
|
14 |
-
|
15 |
-
/**
|
16 |
-
* Completion mode (optional, defaults to "merge")
|
17 |
-
*
|
18 |
-
* Possible values are:
|
19 |
-
* - full: the API and the client will return a full clap file. This is a very convenient and simple mode, but it is also very ineficient, so it should not be used for intensive applications.
|
20 |
-
* - partial: the API and the client will return a partial clap file, containing only the new values and changes. This is useful for real-time applications and streaming.
|
21 |
-
* - merge: the API will return a partial clap file, and the client will return a merge of the original with the new values. This is safe to run, there are no side-effects.
|
22 |
-
* - replace: the API will return a partial clap file, and the client will replace the original. This is the most efficient mode, but it relies on side-effects and inline object updates.
|
23 |
-
*/
|
24 |
-
completionMode?: ClapCompletionMode
|
25 |
-
|
26 |
-
turbo?: boolean
|
27 |
-
|
28 |
-
token?: string
|
29 |
-
}): Promise<ClapProject> {
|
30 |
-
|
31 |
-
if (!clap) { throw new Error(`please provide a valid clap project`) }
|
32 |
-
|
33 |
-
const hasToken = typeof token === "string" && token.length > 0
|
34 |
-
|
35 |
-
const params: Record<string, any> = {}
|
36 |
-
|
37 |
-
if (typeof completionMode === "string") {
|
38 |
-
params.c = completionMode
|
39 |
-
}
|
40 |
-
|
41 |
-
if (turbo) {
|
42 |
-
params.t = "true"
|
43 |
-
}
|
44 |
-
// special trick to not touch the generated
|
45 |
-
// storyboard images that are used by pending videos
|
46 |
-
const idsOfStoryboardsToKeep = clap.segments.map((segment: ClapSegment) => {
|
47 |
-
|
48 |
-
const isPendingVideo = (
|
49 |
-
segment.category === ClapSegmentCategory.VIDEO
|
50 |
-
&&
|
51 |
-
segment.status === ClapSegmentStatus.TO_GENERATE
|
52 |
-
)
|
53 |
-
|
54 |
-
if (!isPendingVideo) { return undefined }
|
55 |
-
|
56 |
-
const storyboardImage: ClapSegment | undefined = filterSegments(
|
57 |
-
ClapSegmentFilteringMode.BOTH,
|
58 |
-
segment,
|
59 |
-
clap.segments,
|
60 |
-
ClapSegmentCategory.IMAGE
|
61 |
-
).at(0)
|
62 |
-
|
63 |
-
return storyboardImage?.id
|
64 |
-
}).filter((x: any) => x) as string[]
|
65 |
-
|
66 |
-
const newClap = await fetchClap(`${aitubeApiUrl}edit/videos?${queryString.stringify(params)}`, {
|
67 |
-
method: "POST",
|
68 |
-
headers: {
|
69 |
-
"Content-Type": "application/x-gzip",
|
70 |
-
...hasToken && {
|
71 |
-
"Authorization": `Bearer ${token}`
|
72 |
-
}
|
73 |
-
},
|
74 |
-
body: await serializeClap(
|
75 |
-
// need a special trick here, to not touch the generated
|
76 |
-
// storyboards that are used by pending videos
|
77 |
-
removeGeneratedAssetUrls(clap, idsOfStoryboardsToKeep)
|
78 |
-
),
|
79 |
-
cache: "no-store",
|
80 |
-
})
|
81 |
-
|
82 |
-
const result = await applyClapCompletion(clap, newClap, completionMode)
|
83 |
-
|
84 |
-
return result
|
85 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/exportClapToVideo.ts
DELETED
@@ -1,60 +0,0 @@
|
|
1 |
-
import queryString from "query-string"
|
2 |
-
import { ClapProject, serializeClap, blobToDataUri } from "@aitube/clap"
|
3 |
-
|
4 |
-
import { aitubeApiUrl } from "@/constants/config"
|
5 |
-
import { defaultExportFormat, SupportedExportFormat } from "@/constants"
|
6 |
-
|
7 |
-
|
8 |
-
export async function exportClapToVideo({
|
9 |
-
clap,
|
10 |
-
format = defaultExportFormat,
|
11 |
-
turbo = false,
|
12 |
-
token,
|
13 |
-
}: {
|
14 |
-
clap: ClapProject
|
15 |
-
|
16 |
-
/**
|
17 |
-
* Desired output video format (defaults to "mp4")
|
18 |
-
*
|
19 |
-
* Can be either "mp4" or "webm"
|
20 |
-
*/
|
21 |
-
format?: SupportedExportFormat
|
22 |
-
|
23 |
-
turbo?: boolean
|
24 |
-
|
25 |
-
token?: string
|
26 |
-
}): Promise<string> {
|
27 |
-
|
28 |
-
if (!clap) { throw new Error(`please provide a clap`) }
|
29 |
-
|
30 |
-
// TODO use an enum instead, and check the enum object
|
31 |
-
if (format !== "mp4" && format !== "webm") { throw new Error(`please provide a valid format ("${format}" is unrecognized)`) }
|
32 |
-
|
33 |
-
const params: Record<string, any> = {}
|
34 |
-
|
35 |
-
params.f = format
|
36 |
-
|
37 |
-
if (turbo) {
|
38 |
-
params.t = "true"
|
39 |
-
}
|
40 |
-
|
41 |
-
const hasToken = typeof token === "string" && token.length > 0
|
42 |
-
|
43 |
-
const res = await fetch(`${aitubeApiUrl}export?${queryString.stringify(params)}`, {
|
44 |
-
method: "POST",
|
45 |
-
headers: {
|
46 |
-
"Content-Type": "application/x-gzip",
|
47 |
-
...hasToken && {
|
48 |
-
"Authorization": `Bearer ${token}`
|
49 |
-
}
|
50 |
-
},
|
51 |
-
body: await serializeClap(clap),
|
52 |
-
cache: "no-store",
|
53 |
-
})
|
54 |
-
|
55 |
-
const blob = await res.blob()
|
56 |
-
|
57 |
-
const dataURL = await blobToDataUri(blob)
|
58 |
-
|
59 |
-
return dataURL
|
60 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/api/index.ts
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
export { createClap } from "./createClap"
|
2 |
-
export { editClapDialogues } from "./editClapDialogues"
|
3 |
-
export { editClapEntities } from "./editClapEntities"
|
4 |
-
export { editClapMusic } from "./editClapMusic"
|
5 |
-
export { editClapSounds } from "./editClapSounds"
|
6 |
-
export { editClapStory } from "./editClapStory"
|
7 |
-
export { editClapStoryboards } from "./editClapStoryboards"
|
8 |
-
export { editClapVideos } from "./editClapVideos"
|
9 |
-
export { exportClapToVideo } from "./exportClapToVideo"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/constants/config.ts
DELETED
@@ -1,16 +0,0 @@
|
|
1 |
-
import { defaultAitubeHostname } from "./defaultValues"
|
2 |
-
|
3 |
-
// we leave the opportunity to override this at runtime
|
4 |
-
export const aitubeUrl = `${
|
5 |
-
process.env.AITUBE_URL ||
|
6 |
-
`https://${defaultAitubeHostname}`
|
7 |
-
}`
|
8 |
-
|
9 |
-
// note: let's keep it simple and only support one version at a time
|
10 |
-
export const aitubeApiVersion = "v1"
|
11 |
-
|
12 |
-
export const aitubeApiUrl = `${
|
13 |
-
aitubeUrl
|
14 |
-
}${
|
15 |
-
aitubeUrl.endsWith("/") ? "" : "/"
|
16 |
-
}api/${aitubeApiVersion}/`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/constants/defaultValues.ts
DELETED
@@ -1,10 +0,0 @@
|
|
1 |
-
// unfortunately, this doesn't work yet due to a redirection issue
|
2 |
-
// const defaultAitubeHostname = "aitube.at"
|
3 |
-
|
4 |
-
// so we have to use the direct space hostname instead
|
5 |
-
export const defaultAitubeHostname = "aitube.at"
|
6 |
-
|
7 |
-
export const defaultClapWidth = 512
|
8 |
-
export const defaultClapHeight = 288
|
9 |
-
|
10 |
-
export const defaultExportFormat = "mp4"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/constants/index.ts
DELETED
@@ -1,17 +0,0 @@
|
|
1 |
-
export {
|
2 |
-
defaultAitubeHostname,
|
3 |
-
defaultClapWidth,
|
4 |
-
defaultClapHeight,
|
5 |
-
defaultExportFormat
|
6 |
-
} from './defaultValues'
|
7 |
-
|
8 |
-
export {
|
9 |
-
aitubeUrl,
|
10 |
-
aitubeApiVersion,
|
11 |
-
aitubeApiUrl
|
12 |
-
} from './config'
|
13 |
-
|
14 |
-
export {
|
15 |
-
ClapEntityPrompt,
|
16 |
-
SupportedExportFormat
|
17 |
-
} from "./types"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/constants/types.ts
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
import { ClapSegmentCategory } from "@aitube/clap"
|
2 |
-
|
3 |
-
export type SupportedExportFormat = "mp4" | "webm"
|
4 |
-
|
5 |
-
export type ClapEntityPrompt = {
|
6 |
-
name: string
|
7 |
-
|
8 |
-
// eg. "character", "location"
|
9 |
-
category: ClapSegmentCategory
|
10 |
-
|
11 |
-
// age of the person, animal or entity (eg. robot, talking spaceship etc)
|
12 |
-
age: string
|
13 |
-
|
14 |
-
// characterization of the person, animal or entity (texture, hair color, gender etc)
|
15 |
-
variant: string
|
16 |
-
|
17 |
-
// region from where the person, animal or entity is coming from (human, mechanical, alien planet, european, south-american etc)
|
18 |
-
region: string
|
19 |
-
|
20 |
-
// identity picture
|
21 |
-
identityImage: string
|
22 |
-
|
23 |
-
// identity voice
|
24 |
-
identityVoice: string
|
25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/index.ts
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
|
2 |
-
export {
|
3 |
-
createClap,
|
4 |
-
editClapDialogues,
|
5 |
-
editClapEntities,
|
6 |
-
editClapMusic,
|
7 |
-
editClapSounds,
|
8 |
-
editClapStory,
|
9 |
-
editClapStoryboards,
|
10 |
-
editClapVideos,
|
11 |
-
exportClapToVideo,
|
12 |
-
} from './api'
|
13 |
-
|
14 |
-
export {
|
15 |
-
defaultAitubeHostname,
|
16 |
-
defaultClapWidth,
|
17 |
-
defaultClapHeight,
|
18 |
-
defaultExportFormat,
|
19 |
-
aitubeUrl,
|
20 |
-
aitubeApiVersion,
|
21 |
-
aitubeApiUrl,
|
22 |
-
ClapEntityPrompt,
|
23 |
-
SupportedExportFormat
|
24 |
-
} from "./constants"
|
25 |
-
|
26 |
-
export {
|
27 |
-
applyClapCompletion,
|
28 |
-
} from "./utils"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/parsers/index.ts
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
export { parseEntityPrompt } from "./parseEntityPrompt"
|
2 |
-
export { parseString } from "./parseString"
|
3 |
-
export { parseStringArray } from "./parseStringArray"
|
|
|
|
|
|
|
|
packages/api-client/src/parsers/parseEntityPrompt.ts
DELETED
@@ -1,30 +0,0 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
import { ClapEntityPrompt } from "@/constants/types"
|
4 |
-
|
5 |
-
import { parseString } from "./parseString"
|
6 |
-
import { ClapSegmentCategory } from "@aitube/clap"
|
7 |
-
|
8 |
-
export function parseEntityPrompt(entityPrompt: Partial<ClapEntityPrompt> = {}): ClapEntityPrompt {
|
9 |
-
|
10 |
-
return {
|
11 |
-
name: parseString(entityPrompt?.name),
|
12 |
-
|
13 |
-
category: parseString(entityPrompt?.category) as ClapSegmentCategory,
|
14 |
-
|
15 |
-
// age of the person, animal or entity (eg. robot, talking spaceship etc)
|
16 |
-
age: parseString(entityPrompt?.age),
|
17 |
-
|
18 |
-
// characterization of the person, animal or entity (texture, hair color, gender etc)
|
19 |
-
variant: parseString(entityPrompt?.variant),
|
20 |
-
|
21 |
-
// region from where the person, animal or entity is coming from (human, mechanical, alien planet, european, south-american etc)
|
22 |
-
region: parseString(entityPrompt?.region),
|
23 |
-
|
24 |
-
// identity picture
|
25 |
-
identityImage: parseString(entityPrompt?.identityImage),
|
26 |
-
|
27 |
-
// identity voice
|
28 |
-
identityVoice: parseString(entityPrompt?.identityVoice),
|
29 |
-
}
|
30 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/parsers/parseString.ts
DELETED
@@ -1,7 +0,0 @@
|
|
1 |
-
export function parseString(input?: any, defaultValue?: string): string {
|
2 |
-
const defValue = `${defaultValue || ""}`
|
3 |
-
|
4 |
-
if (typeof input !== "string") { return defValue }
|
5 |
-
|
6 |
-
return input || defValue
|
7 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/parsers/parseStringArray.ts
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
export function parseStringArray(something: any): string[] {
|
2 |
-
let result: string[] = []
|
3 |
-
if (typeof something === "string") {
|
4 |
-
result = [something]
|
5 |
-
} else if (Array.isArray(something)) {
|
6 |
-
result = something.map(thing => typeof thing === "string" ? thing : "").filter(x => x)
|
7 |
-
}
|
8 |
-
return result
|
9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/utils/applyClapCompletion.ts
DELETED
@@ -1,21 +0,0 @@
|
|
1 |
-
import { ClapCompletionMode, ClapProject, updateClap } from "@aitube/clap"
|
2 |
-
|
3 |
-
export async function applyClapCompletion(
|
4 |
-
existingClap: ClapProject,
|
5 |
-
newerClap: ClapProject,
|
6 |
-
clapCompletionMode: ClapCompletionMode
|
7 |
-
): Promise<ClapProject> {
|
8 |
-
// in both those mode we leave full control to what is inside "newerClap"
|
9 |
-
if (clapCompletionMode === ClapCompletionMode.FULL || clapCompletionMode === ClapCompletionMode.PARTIAL) {
|
10 |
-
return newerClap
|
11 |
-
}
|
12 |
-
|
13 |
-
// else we are in ClapCompletionMode.MERGE or ClapCompletionMode.REPLACE
|
14 |
-
const result = await updateClap(existingClap, newerClap, {
|
15 |
-
// the newer clap meta may contain incomplete information
|
16 |
-
overwriteMeta: false,
|
17 |
-
inlineReplace: clapCompletionMode === ClapCompletionMode.REPLACE
|
18 |
-
})
|
19 |
-
|
20 |
-
return result
|
21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/src/utils/index.ts
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
export { applyClapCompletion } from "./applyClapCompletion"
|
|
|
|
packages/api-client/tsconfig.json
DELETED
@@ -1,31 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"compilerOptions": {
|
3 |
-
"outDir": "./dist",
|
4 |
-
"rootDir": "./src",
|
5 |
-
"baseUrl": "./",
|
6 |
-
"paths": {
|
7 |
-
"@/*": ["src/*"]
|
8 |
-
},
|
9 |
-
"lib": ["ESNext", "DOM"],
|
10 |
-
"module": "esnext",
|
11 |
-
"target": "esnext",
|
12 |
-
"moduleResolution": "bundler",
|
13 |
-
"moduleDetection": "force",
|
14 |
-
"allowImportingTsExtensions": true,
|
15 |
-
"noEmit": true,
|
16 |
-
"composite": true,
|
17 |
-
"strict": true,
|
18 |
-
"downlevelIteration": true,
|
19 |
-
"skipLibCheck": true,
|
20 |
-
"jsx": "react-jsx",
|
21 |
-
"allowSyntheticDefaultImports": true,
|
22 |
-
"forceConsistentCasingInFileNames": true,
|
23 |
-
"allowJs": true,
|
24 |
-
"types": [
|
25 |
-
"bun-types"
|
26 |
-
]
|
27 |
-
},
|
28 |
-
"include": [
|
29 |
-
"src/**/*.ts"
|
30 |
-
]
|
31 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/api-client/tsconfig.types.json
DELETED
@@ -1,13 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"extends": "./tsconfig.json",
|
3 |
-
"compilerOptions": {
|
4 |
-
"noEmit": false,
|
5 |
-
"emitDeclarationOnly": true,
|
6 |
-
"declaration": true,
|
7 |
-
"outDir": "./dist",
|
8 |
-
"rootDir": "./src",
|
9 |
-
},
|
10 |
-
"include": [
|
11 |
-
"src/**/*.ts"
|
12 |
-
]
|
13 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/app/.nvmrc
CHANGED
@@ -1 +1 @@
|
|
1 |
-
v20.
|
|
|
1 |
+
v20.17.0
|
packages/app/package.json
CHANGED
@@ -35,7 +35,7 @@
|
|
35 |
"electron:make": "bun run build && electron-forge make"
|
36 |
},
|
37 |
"dependencies": {
|
38 |
-
"@aitube/
|
39 |
"@aitube/broadway": "workspace:*",
|
40 |
"@aitube/clap": "workspace:*",
|
41 |
"@aitube/clapper-services": "workspace:*",
|
@@ -44,17 +44,17 @@
|
|
44 |
"@fal-ai/serverless-client": "^0.14.2",
|
45 |
"@ffmpeg/ffmpeg": "^0.12.10",
|
46 |
"@ffmpeg/util": "^0.12.1",
|
47 |
-
"@gradio/client": "^1.5.
|
48 |
"@huggingface/hub": "^0.15.1",
|
49 |
"@huggingface/inference": "^2.8.0",
|
50 |
-
"@huggingface/transformers": "3.0.0-alpha.
|
51 |
-
"@langchain/anthropic": "^0.2.
|
52 |
"@langchain/cohere": "^0.2.2",
|
53 |
-
"@langchain/core": "^0.2.
|
54 |
-
"@langchain/google-vertexai": "^0.0.
|
55 |
-
"@langchain/groq": "^0.0.
|
56 |
-
"@langchain/mistralai": "^0.0.
|
57 |
-
"@langchain/openai": "^0.2.
|
58 |
"@monaco-editor/react": "^4.6.0",
|
59 |
"@radix-ui/react-accordion": "^1.1.2",
|
60 |
"@radix-ui/react-avatar": "^1.0.4",
|
@@ -82,12 +82,12 @@
|
|
82 |
"@react-three/fiber": "^8.16.6",
|
83 |
"@react-three/uikit": "^0.3.4",
|
84 |
"@react-three/uikit-lucide": "^0.3.4",
|
85 |
-
"@saintno/comfyui-sdk": "
|
86 |
"@tailwindcss/container-queries": "^0.1.1",
|
87 |
"@types/dom-speech-recognition": "^0.0.4",
|
88 |
"@types/pngjs": "^6.0.5",
|
89 |
-
"@xyflow/react": "^12.0
|
90 |
-
"autoprefixer": "10.4.
|
91 |
"base64-arraybuffer": "^1.0.2",
|
92 |
"bellhop-iframe": "^3.5.0",
|
93 |
"civitai": "^0.1.15",
|
@@ -99,18 +99,18 @@
|
|
99 |
"dotenv": "^16.4.5",
|
100 |
"fflate": "^0.8.2",
|
101 |
"fluent-ffmpeg": "^2.1.3",
|
102 |
-
"framer-motion": "11.
|
103 |
"fs-extra": "^11.2.0",
|
104 |
"is-hotkey": "^0.2.0",
|
105 |
-
"lucide-react": "^0.
|
106 |
"mediainfo.js": "^0.3.2",
|
107 |
"mlt-xml": "^2.0.2",
|
108 |
-
"monaco-editor": "^0.
|
109 |
-
"next": "
|
110 |
"next-themes": "^0.3.0",
|
111 |
"pngjs": "^7.0.0",
|
112 |
-
"qs": "^6.
|
113 |
-
"query-string": "^9.
|
114 |
"react": "^18.3.1",
|
115 |
"react-device-frameset": "^1.3.4",
|
116 |
"react-dnd": "^16.0.1",
|
@@ -153,6 +153,7 @@
|
|
153 |
"@testing-library/react": "^16.0.0",
|
154 |
"@types/fluent-ffmpeg": "^2.1.24",
|
155 |
"@types/is-hotkey": "^0.1.10",
|
|
|
156 |
"@types/node": "^20",
|
157 |
"@types/react": "^18",
|
158 |
"@types/react-dom": "^18",
|
|
|
35 |
"electron:make": "bun run build && electron-forge make"
|
36 |
},
|
37 |
"dependencies": {
|
38 |
+
"@aitube/client": "workspace:*",
|
39 |
"@aitube/broadway": "workspace:*",
|
40 |
"@aitube/clap": "workspace:*",
|
41 |
"@aitube/clapper-services": "workspace:*",
|
|
|
44 |
"@fal-ai/serverless-client": "^0.14.2",
|
45 |
"@ffmpeg/ffmpeg": "^0.12.10",
|
46 |
"@ffmpeg/util": "^0.12.1",
|
47 |
+
"@gradio/client": "^1.5.1",
|
48 |
"@huggingface/hub": "^0.15.1",
|
49 |
"@huggingface/inference": "^2.8.0",
|
50 |
+
"@huggingface/transformers": "3.0.0-alpha.14",
|
51 |
+
"@langchain/anthropic": "^0.2.15",
|
52 |
"@langchain/cohere": "^0.2.2",
|
53 |
+
"@langchain/core": "^0.2.31",
|
54 |
+
"@langchain/google-vertexai": "^0.0.27",
|
55 |
+
"@langchain/groq": "^0.0.17",
|
56 |
+
"@langchain/mistralai": "^0.0.29",
|
57 |
+
"@langchain/openai": "^0.2.8",
|
58 |
"@monaco-editor/react": "^4.6.0",
|
59 |
"@radix-ui/react-accordion": "^1.1.2",
|
60 |
"@radix-ui/react-avatar": "^1.0.4",
|
|
|
82 |
"@react-three/fiber": "^8.16.6",
|
83 |
"@react-three/uikit": "^0.3.4",
|
84 |
"@react-three/uikit-lucide": "^0.3.4",
|
85 |
+
"@saintno/comfyui-sdk": "0.1.20",
|
86 |
"@tailwindcss/container-queries": "^0.1.1",
|
87 |
"@types/dom-speech-recognition": "^0.0.4",
|
88 |
"@types/pngjs": "^6.0.5",
|
89 |
+
"@xyflow/react": "^12.2.0",
|
90 |
+
"autoprefixer": "10.4.20",
|
91 |
"base64-arraybuffer": "^1.0.2",
|
92 |
"bellhop-iframe": "^3.5.0",
|
93 |
"civitai": "^0.1.15",
|
|
|
99 |
"dotenv": "^16.4.5",
|
100 |
"fflate": "^0.8.2",
|
101 |
"fluent-ffmpeg": "^2.1.3",
|
102 |
+
"framer-motion": "11.3.31",
|
103 |
"fs-extra": "^11.2.0",
|
104 |
"is-hotkey": "^0.2.0",
|
105 |
+
"lucide-react": "^0.438.0",
|
106 |
"mediainfo.js": "^0.3.2",
|
107 |
"mlt-xml": "^2.0.2",
|
108 |
+
"monaco-editor": "^0.51.0",
|
109 |
+
"next": "14.2.7",
|
110 |
"next-themes": "^0.3.0",
|
111 |
"pngjs": "^7.0.0",
|
112 |
+
"qs": "^6.13.0",
|
113 |
+
"query-string": "^9.1.0",
|
114 |
"react": "^18.3.1",
|
115 |
"react-device-frameset": "^1.3.4",
|
116 |
"react-dnd": "^16.0.1",
|
|
|
153 |
"@testing-library/react": "^16.0.0",
|
154 |
"@types/fluent-ffmpeg": "^2.1.24",
|
155 |
"@types/is-hotkey": "^0.1.10",
|
156 |
+
"@types/lodash": "4.17.7",
|
157 |
"@types/node": "^20",
|
158 |
"@types/react": "^18",
|
159 |
"@types/react-dom": "^18",
|
packages/app/src/app/api/resolve/providers/aitube/index.ts
CHANGED
@@ -12,7 +12,7 @@ import {
|
|
12 |
editClapSounds,
|
13 |
editClapStoryboards,
|
14 |
editClapVideos,
|
15 |
-
} from '@aitube/
|
16 |
|
17 |
import { getWorkflowInputValues } from '../getWorkflowInputValues'
|
18 |
|
|
|
12 |
editClapSounds,
|
13 |
editClapStoryboards,
|
14 |
editClapVideos,
|
15 |
+
} from '@aitube/client'
|
16 |
|
17 |
import { getWorkflowInputValues } from '../getWorkflowInputValues'
|
18 |
|
packages/app/src/app/api/resolve/providers/comfy-replicate/index.ts
CHANGED
@@ -5,7 +5,7 @@ import {
|
|
5 |
} from '@aitube/clap'
|
6 |
|
7 |
import { ResolveRequest } from '@aitube/clapper-services'
|
8 |
-
|
9 |
import { runWorkflow } from './runWorkflow'
|
10 |
import { TimelineSegment } from '@aitube/timeline'
|
11 |
|
|
|
5 |
} from '@aitube/clap'
|
6 |
|
7 |
import { ResolveRequest } from '@aitube/clapper-services'
|
8 |
+
|
9 |
import { runWorkflow } from './runWorkflow'
|
10 |
import { TimelineSegment } from '@aitube/timeline'
|
11 |
|
packages/app/src/app/api/resolve/providers/comfy-replicate/runWorkflow.ts
CHANGED
@@ -1,4 +1,8 @@
|
|
1 |
-
import Replicate from 'replicate'
|
|
|
|
|
|
|
|
|
2 |
|
3 |
export async function runWorkflow({
|
4 |
apiKey,
|
|
|
1 |
+
// import Replicate from 'replicate'
|
2 |
+
// fix for the error:
|
3 |
+
// ../../node_modules/replicate/lib/util.js
|
4 |
+
// Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
|
5 |
+
const Replicate = require('replicate')
|
6 |
|
7 |
export async function runWorkflow({
|
8 |
apiKey,
|
packages/app/src/app/api/resolve/providers/comfyui/convertComfyUiWorkflowApiToClapWorkflow.ts
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ClapWorkflow,
|
3 |
+
ClapWorkflowCategory,
|
4 |
+
ClapWorkflowEngine,
|
5 |
+
ClapWorkflowProvider,
|
6 |
+
} from '@aitube/clap'
|
7 |
+
|
8 |
+
import { getInputsFromComfyUiWorkflow } from './getInputsFromComfyUiWorkflow'
|
9 |
+
|
10 |
+
export function convertComfyUiWorkflowApiToClapWorkflow(
|
11 |
+
workflowString: string,
|
12 |
+
category: ClapWorkflowCategory = ClapWorkflowCategory.IMAGE_GENERATION
|
13 |
+
): ClapWorkflow {
|
14 |
+
try {
|
15 |
+
const { inputFields, inputValues } = getInputsFromComfyUiWorkflow(
|
16 |
+
workflowString,
|
17 |
+
category
|
18 |
+
)
|
19 |
+
switch (category) {
|
20 |
+
case ClapWorkflowCategory.VIDEO_GENERATION: {
|
21 |
+
return {
|
22 |
+
id: 'comfyui://settings.comfyWorkflowForVideo',
|
23 |
+
label: 'Custom Video Workflow',
|
24 |
+
description: 'Custom ComfyUI workflow to generate videos',
|
25 |
+
tags: ['custom', 'video generation'],
|
26 |
+
author: 'You',
|
27 |
+
thumbnailUrl: '',
|
28 |
+
nonCommercial: false,
|
29 |
+
engine: ClapWorkflowEngine.COMFYUI_WORKFLOW,
|
30 |
+
provider: ClapWorkflowProvider.COMFYUI,
|
31 |
+
category,
|
32 |
+
data: workflowString,
|
33 |
+
schema: '',
|
34 |
+
inputFields,
|
35 |
+
inputValues,
|
36 |
+
}
|
37 |
+
}
|
38 |
+
default: {
|
39 |
+
return {
|
40 |
+
id: 'comfyui://settings.comfyWorkflowForImage',
|
41 |
+
label: 'Custom Image Workflow',
|
42 |
+
description: 'Custom ComfyUI workflow to generate images',
|
43 |
+
tags: ['custom', 'image generation'],
|
44 |
+
author: 'You',
|
45 |
+
thumbnailUrl: '',
|
46 |
+
nonCommercial: false,
|
47 |
+
engine: ClapWorkflowEngine.COMFYUI_WORKFLOW,
|
48 |
+
provider: ClapWorkflowProvider.COMFYUI,
|
49 |
+
category,
|
50 |
+
data: workflowString,
|
51 |
+
schema: '',
|
52 |
+
inputFields,
|
53 |
+
inputValues,
|
54 |
+
}
|
55 |
+
}
|
56 |
+
}
|
57 |
+
} catch (e) {
|
58 |
+
throw e
|
59 |
+
}
|
60 |
+
}
|
packages/app/src/app/api/resolve/providers/comfyui/createPromptBuilder.spec.ts
ADDED
@@ -0,0 +1,595 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { expect, test } from 'vitest'
|
2 |
+
import { createPromptBuilder } from './createPromptBuilder'
|
3 |
+
|
4 |
+
// Default workflow used by ComfyUI, downloaded for API
|
5 |
+
const workflowRaw = {
|
6 |
+
'3': {
|
7 |
+
inputs: {
|
8 |
+
seed: 156680208700286,
|
9 |
+
steps: 20,
|
10 |
+
cfg: 8,
|
11 |
+
sampler_name: 'euler',
|
12 |
+
scheduler: 'normal',
|
13 |
+
denoise: 1,
|
14 |
+
model: ['4', 0],
|
15 |
+
positive: ['6', 0],
|
16 |
+
negative: ['7', 0],
|
17 |
+
latent_image: ['5', 0],
|
18 |
+
},
|
19 |
+
class_type: 'KSampler',
|
20 |
+
_meta: {
|
21 |
+
title: 'KSampler',
|
22 |
+
},
|
23 |
+
},
|
24 |
+
'4': {
|
25 |
+
inputs: {
|
26 |
+
ckpt_name: 'v1-5-pruned-emaonly.ckpt',
|
27 |
+
},
|
28 |
+
class_type: 'CheckpointLoaderSimple',
|
29 |
+
_meta: {
|
30 |
+
title: 'Load Checkpoint',
|
31 |
+
},
|
32 |
+
},
|
33 |
+
'5': {
|
34 |
+
inputs: {
|
35 |
+
width: 512,
|
36 |
+
height: 512,
|
37 |
+
batch_size: 1,
|
38 |
+
},
|
39 |
+
class_type: 'EmptyLatentImage',
|
40 |
+
_meta: {
|
41 |
+
title: 'Empty Latent Image',
|
42 |
+
},
|
43 |
+
},
|
44 |
+
'6': {
|
45 |
+
inputs: {
|
46 |
+
text: 'beautiful scenery nature glass bottle landscape, , purple galaxy bottle,',
|
47 |
+
clip: ['4', 1],
|
48 |
+
},
|
49 |
+
class_type: 'CLIPTextEncode',
|
50 |
+
_meta: {
|
51 |
+
title: 'CLIP Text Encode (Prompt)',
|
52 |
+
},
|
53 |
+
},
|
54 |
+
'7': {
|
55 |
+
inputs: {
|
56 |
+
text: 'text, watermark',
|
57 |
+
clip: ['4', 1],
|
58 |
+
},
|
59 |
+
class_type: 'CLIPTextEncode',
|
60 |
+
_meta: {
|
61 |
+
title: 'CLIP Text Encode (Prompt)',
|
62 |
+
},
|
63 |
+
},
|
64 |
+
'8': {
|
65 |
+
inputs: {
|
66 |
+
samples: ['3', 0],
|
67 |
+
vae: ['4', 2],
|
68 |
+
},
|
69 |
+
class_type: 'VAEDecode',
|
70 |
+
_meta: {
|
71 |
+
title: 'VAE Decode',
|
72 |
+
},
|
73 |
+
},
|
74 |
+
'9': {
|
75 |
+
inputs: {
|
76 |
+
filename_prefix: 'ComfyUI',
|
77 |
+
images: ['8', 0],
|
78 |
+
},
|
79 |
+
class_type: 'SaveImage',
|
80 |
+
_meta: {
|
81 |
+
title: 'Save Image',
|
82 |
+
},
|
83 |
+
},
|
84 |
+
}
|
85 |
+
|
86 |
+
// Example workflow object using @clapper/- tokens
|
87 |
+
const workflowRawWithTokens = {
|
88 |
+
'3': {
|
89 |
+
inputs: {
|
90 |
+
seed: 156680208700286,
|
91 |
+
steps: 20,
|
92 |
+
cfg: 8,
|
93 |
+
sampler_name: 'euler',
|
94 |
+
scheduler: 'normal',
|
95 |
+
denoise: 1,
|
96 |
+
model: ['4', 0],
|
97 |
+
positive: ['6', 0],
|
98 |
+
negative: ['7', 0],
|
99 |
+
latent_image: ['5', 0],
|
100 |
+
},
|
101 |
+
class_type: 'KSampler',
|
102 |
+
_meta: {
|
103 |
+
title: 'KSampler',
|
104 |
+
},
|
105 |
+
},
|
106 |
+
'4': {
|
107 |
+
inputs: {
|
108 |
+
ckpt_name: 'v1-5-pruned-emaonly.ckpt',
|
109 |
+
},
|
110 |
+
class_type: 'CheckpointLoaderSimple',
|
111 |
+
_meta: {
|
112 |
+
title: 'Load Checkpoint',
|
113 |
+
},
|
114 |
+
},
|
115 |
+
'5': {
|
116 |
+
inputs: {
|
117 |
+
width: 512,
|
118 |
+
height: 512,
|
119 |
+
batch_size: 1,
|
120 |
+
},
|
121 |
+
class_type: 'EmptyLatentImage',
|
122 |
+
_meta: {
|
123 |
+
title: 'Empty Latent Image',
|
124 |
+
},
|
125 |
+
},
|
126 |
+
'6': {
|
127 |
+
inputs: {
|
128 |
+
text: '@clapper/prompt',
|
129 |
+
clip: ['4', 1],
|
130 |
+
},
|
131 |
+
class_type: 'CLIPTextEncode',
|
132 |
+
_meta: {
|
133 |
+
title: 'CLIP Text Encode (Prompt)',
|
134 |
+
},
|
135 |
+
},
|
136 |
+
'7': {
|
137 |
+
inputs: {
|
138 |
+
text: '@clapper/negative',
|
139 |
+
clip: ['4', 1],
|
140 |
+
},
|
141 |
+
class_type: 'CLIPTextEncode',
|
142 |
+
_meta: {
|
143 |
+
title: 'CLIP Text Encode (Prompt)',
|
144 |
+
},
|
145 |
+
},
|
146 |
+
'8': {
|
147 |
+
inputs: {
|
148 |
+
samples: ['3', 0],
|
149 |
+
vae: ['4', 2],
|
150 |
+
},
|
151 |
+
class_type: 'VAEDecode',
|
152 |
+
_meta: {
|
153 |
+
title: 'VAE Decode',
|
154 |
+
},
|
155 |
+
},
|
156 |
+
'9': {
|
157 |
+
inputs: {
|
158 |
+
filename_prefix: 'ComfyUI',
|
159 |
+
images: ['8', 0],
|
160 |
+
},
|
161 |
+
class_type: 'SaveImage',
|
162 |
+
_meta: {
|
163 |
+
title: 'Save Image',
|
164 |
+
},
|
165 |
+
},
|
166 |
+
}
|
167 |
+
|
168 |
+
test('should return all nodes that have inputs', () => {
|
169 |
+
const nodesWithInputs = new ComfyUIWorkflowApiGraph(
|
170 |
+
workflowRaw
|
171 |
+
).getNodesWithInputs()
|
172 |
+
|
173 |
+
// Expect nodes 3, 4, 5, 6, 7, 8, and 9 to have inputs
|
174 |
+
expect(nodesWithInputs).toHaveLength(7)
|
175 |
+
expect(nodesWithInputs).toEqual(
|
176 |
+
expect.arrayContaining([
|
177 |
+
expect.objectContaining({ id: '3' }),
|
178 |
+
expect.objectContaining({ id: '4' }),
|
179 |
+
expect.objectContaining({ id: '5' }),
|
180 |
+
expect.objectContaining({ id: '6' }),
|
181 |
+
expect.objectContaining({ id: '7' }),
|
182 |
+
expect.objectContaining({ id: '8' }),
|
183 |
+
expect.objectContaining({ id: '9' }),
|
184 |
+
])
|
185 |
+
)
|
186 |
+
})
|
187 |
+
|
188 |
+
test('should return the correct output node', () => {
|
189 |
+
const outputNode = new ComfyUIWorkflowApiGraph(workflowRaw).getOutputNode()
|
190 |
+
|
191 |
+
expect(outputNode).toEqual({
|
192 |
+
id: '9',
|
193 |
+
inputs: {
|
194 |
+
filename_prefix: 'ComfyUI',
|
195 |
+
images: ['8', 0],
|
196 |
+
},
|
197 |
+
class_type: 'SaveImage',
|
198 |
+
_meta: {
|
199 |
+
title: 'Save Image',
|
200 |
+
},
|
201 |
+
})
|
202 |
+
})
|
203 |
+
|
204 |
+
test('should build the correct graph from the workflow', () => {
|
205 |
+
const { adjList, dependencyList, inDegree } = new ComfyUIWorkflowApiGraph(
|
206 |
+
workflowRaw
|
207 |
+
).getGraphData()
|
208 |
+
|
209 |
+
expect(adjList['3']).toEqual(['8'])
|
210 |
+
expect(adjList['8']).toEqual(['9'])
|
211 |
+
expect(inDegree['3']).toBe(4)
|
212 |
+
expect(dependencyList['3']).toEqual([
|
213 |
+
{
|
214 |
+
from: '4',
|
215 |
+
inputName: 'model',
|
216 |
+
},
|
217 |
+
{
|
218 |
+
from: '6',
|
219 |
+
inputName: 'positive',
|
220 |
+
},
|
221 |
+
{ from: '7', inputName: 'negative' },
|
222 |
+
{ from: '5', inputName: 'latent_image' },
|
223 |
+
])
|
224 |
+
expect(inDegree['9']).toBe(1)
|
225 |
+
})
|
226 |
+
|
227 |
+
test('should return the correct inputs by node id', () => {
|
228 |
+
const workflow = new ComfyUIWorkflowApiGraph(workflowRaw)
|
229 |
+
|
230 |
+
expect(workflow.getInputsByNodeId('3')).toEqual([
|
231 |
+
{
|
232 |
+
type: 'number',
|
233 |
+
name: 'seed',
|
234 |
+
value: 156680208700286,
|
235 |
+
id: '3.inputs.seed',
|
236 |
+
node: {
|
237 |
+
id: '3',
|
238 |
+
name: 'KSampler',
|
239 |
+
type: 'KSampler',
|
240 |
+
},
|
241 |
+
},
|
242 |
+
{
|
243 |
+
type: 'number',
|
244 |
+
name: 'steps',
|
245 |
+
value: 20,
|
246 |
+
id: '3.inputs.steps',
|
247 |
+
node: {
|
248 |
+
id: '3',
|
249 |
+
name: 'KSampler',
|
250 |
+
type: 'KSampler',
|
251 |
+
},
|
252 |
+
},
|
253 |
+
{
|
254 |
+
type: 'number',
|
255 |
+
name: 'cfg',
|
256 |
+
value: 8,
|
257 |
+
id: '3.inputs.cfg',
|
258 |
+
node: {
|
259 |
+
id: '3',
|
260 |
+
name: 'KSampler',
|
261 |
+
type: 'KSampler',
|
262 |
+
},
|
263 |
+
},
|
264 |
+
{
|
265 |
+
type: 'string',
|
266 |
+
name: 'sampler_name',
|
267 |
+
value: 'euler',
|
268 |
+
id: '3.inputs.sampler_name',
|
269 |
+
node: {
|
270 |
+
id: '3',
|
271 |
+
name: 'KSampler',
|
272 |
+
type: 'KSampler',
|
273 |
+
},
|
274 |
+
},
|
275 |
+
{
|
276 |
+
type: 'string',
|
277 |
+
name: 'scheduler',
|
278 |
+
value: 'normal',
|
279 |
+
id: '3.inputs.scheduler',
|
280 |
+
node: {
|
281 |
+
id: '3',
|
282 |
+
name: 'KSampler',
|
283 |
+
type: 'KSampler',
|
284 |
+
},
|
285 |
+
},
|
286 |
+
{
|
287 |
+
type: 'number',
|
288 |
+
name: 'denoise',
|
289 |
+
value: 1,
|
290 |
+
id: '3.inputs.denoise',
|
291 |
+
node: {
|
292 |
+
id: '3',
|
293 |
+
name: 'KSampler',
|
294 |
+
type: 'KSampler',
|
295 |
+
},
|
296 |
+
},
|
297 |
+
])
|
298 |
+
|
299 |
+
expect(workflow.getInputsByNodeId('4')).toEqual([
|
300 |
+
{
|
301 |
+
type: 'string',
|
302 |
+
name: 'ckpt_name',
|
303 |
+
value: 'v1-5-pruned-emaonly.ckpt',
|
304 |
+
id: '4.inputs.ckpt_name',
|
305 |
+
node: {
|
306 |
+
id: '4',
|
307 |
+
name: 'Load Checkpoint',
|
308 |
+
type: 'CheckpointLoaderSimple',
|
309 |
+
},
|
310 |
+
},
|
311 |
+
])
|
312 |
+
|
313 |
+
expect(workflow.getInputsByNodeId('5')).toEqual([
|
314 |
+
{
|
315 |
+
type: 'number',
|
316 |
+
name: 'width',
|
317 |
+
value: 512,
|
318 |
+
id: '5.inputs.width',
|
319 |
+
node: {
|
320 |
+
id: '5',
|
321 |
+
name: 'Empty Latent Image',
|
322 |
+
type: 'EmptyLatentImage',
|
323 |
+
},
|
324 |
+
},
|
325 |
+
{
|
326 |
+
type: 'number',
|
327 |
+
name: 'height',
|
328 |
+
value: 512,
|
329 |
+
id: '5.inputs.height',
|
330 |
+
node: {
|
331 |
+
id: '5',
|
332 |
+
name: 'Empty Latent Image',
|
333 |
+
type: 'EmptyLatentImage',
|
334 |
+
},
|
335 |
+
},
|
336 |
+
{
|
337 |
+
type: 'number',
|
338 |
+
name: 'batch_size',
|
339 |
+
value: 1,
|
340 |
+
id: '5.inputs.batch_size',
|
341 |
+
node: {
|
342 |
+
id: '5',
|
343 |
+
name: 'Empty Latent Image',
|
344 |
+
type: 'EmptyLatentImage',
|
345 |
+
},
|
346 |
+
},
|
347 |
+
])
|
348 |
+
|
349 |
+
expect(workflow.getInputsByNodeId('6')).toEqual([
|
350 |
+
{
|
351 |
+
type: 'string',
|
352 |
+
name: 'text',
|
353 |
+
value:
|
354 |
+
'beautiful scenery nature glass bottle landscape, , purple galaxy bottle,',
|
355 |
+
id: '6.inputs.text',
|
356 |
+
node: {
|
357 |
+
id: '6',
|
358 |
+
name: 'CLIP Text Encode (Prompt)',
|
359 |
+
type: 'CLIPTextEncode',
|
360 |
+
},
|
361 |
+
},
|
362 |
+
])
|
363 |
+
|
364 |
+
const nonExistentNodeInputs = workflow.getInputsByNodeId('99')
|
365 |
+
expect(nonExistentNodeInputs).toBeNull()
|
366 |
+
})
|
367 |
+
|
368 |
+
test('should detect the correct positive and negative prompt inputs', () => {
|
369 |
+
const workflow = new ComfyUIWorkflowApiGraph(workflowRaw)
|
370 |
+
const positivePromptInput = findPromptInputsFromWorkflow(workflow)
|
371 |
+
const negativePromptInput = findNegativePromptInputsFromWorkflow(workflow)
|
372 |
+
|
373 |
+
expect(positivePromptInput).toEqual([
|
374 |
+
{
|
375 |
+
id: '6.inputs.text',
|
376 |
+
node: {
|
377 |
+
id: '6',
|
378 |
+
name: 'CLIP Text Encode (Prompt)',
|
379 |
+
type: 'CLIPTextEncode',
|
380 |
+
},
|
381 |
+
name: 'text',
|
382 |
+
type: 'string',
|
383 |
+
value:
|
384 |
+
'beautiful scenery nature glass bottle landscape, , purple galaxy bottle,',
|
385 |
+
},
|
386 |
+
])
|
387 |
+
|
388 |
+
expect(negativePromptInput).toEqual([
|
389 |
+
{
|
390 |
+
id: '7.inputs.text',
|
391 |
+
node: {
|
392 |
+
id: '7',
|
393 |
+
name: 'CLIP Text Encode (Prompt)',
|
394 |
+
type: 'CLIPTextEncode',
|
395 |
+
},
|
396 |
+
name: 'text',
|
397 |
+
type: 'string',
|
398 |
+
value: 'text, watermark',
|
399 |
+
},
|
400 |
+
])
|
401 |
+
|
402 |
+
expect(positivePromptInput).not.toEqual(
|
403 |
+
expect.arrayContaining([expect.objectContaining({ name: 'steps' })])
|
404 |
+
)
|
405 |
+
|
406 |
+
expect(negativePromptInput).not.toEqual(
|
407 |
+
expect.arrayContaining([expect.objectContaining({ name: 'cfg' })])
|
408 |
+
)
|
409 |
+
})
|
410 |
+
|
411 |
+
test('should detect the correct positive and negative prompt inputs using clapper tokens', () => {
|
412 |
+
const workflow = new ComfyUIWorkflowApiGraph(workflowRawWithTokens)
|
413 |
+
const positivePromptInput = findPromptInputsFromWorkflow(workflow)
|
414 |
+
const negativePromptInput = findNegativePromptInputsFromWorkflow(workflow)
|
415 |
+
|
416 |
+
expect(positivePromptInput).toEqual([
|
417 |
+
{
|
418 |
+
id: '6.inputs.text',
|
419 |
+
node: {
|
420 |
+
id: '6',
|
421 |
+
name: 'CLIP Text Encode (Prompt)',
|
422 |
+
type: 'CLIPTextEncode',
|
423 |
+
},
|
424 |
+
type: 'string',
|
425 |
+
name: 'text',
|
426 |
+
value: '@clapper/prompt',
|
427 |
+
},
|
428 |
+
])
|
429 |
+
|
430 |
+
expect(negativePromptInput).toEqual([
|
431 |
+
{
|
432 |
+
id: '7.inputs.text',
|
433 |
+
node: {
|
434 |
+
id: '7',
|
435 |
+
name: 'CLIP Text Encode (Prompt)',
|
436 |
+
type: 'CLIPTextEncode',
|
437 |
+
},
|
438 |
+
type: 'string',
|
439 |
+
name: 'text',
|
440 |
+
value: '@clapper/negative',
|
441 |
+
},
|
442 |
+
])
|
443 |
+
|
444 |
+
expect(positivePromptInput).not.toEqual(
|
445 |
+
expect.arrayContaining([expect.objectContaining({ name: 'steps' })])
|
446 |
+
)
|
447 |
+
expect(negativePromptInput).not.toEqual(
|
448 |
+
expect.arrayContaining([expect.objectContaining({ name: 'cfg' })])
|
449 |
+
)
|
450 |
+
})
|
451 |
+
|
452 |
+
test('should correctly search workflow inputs', () => {
|
453 |
+
const workflow = new ComfyUIWorkflowApiGraph(workflowRaw)
|
454 |
+
expect(
|
455 |
+
workflow.findInput({
|
456 |
+
name: 'text',
|
457 |
+
})
|
458 |
+
).toEqual([
|
459 |
+
{
|
460 |
+
id: '6.inputs.text',
|
461 |
+
node: {
|
462 |
+
id: '6',
|
463 |
+
name: 'CLIP Text Encode (Prompt)',
|
464 |
+
type: 'CLIPTextEncode',
|
465 |
+
},
|
466 |
+
name: 'text',
|
467 |
+
type: 'string',
|
468 |
+
value:
|
469 |
+
'beautiful scenery nature glass bottle landscape, , purple galaxy bottle,',
|
470 |
+
},
|
471 |
+
{
|
472 |
+
id: '7.inputs.text',
|
473 |
+
node: {
|
474 |
+
id: '7',
|
475 |
+
name: 'CLIP Text Encode (Prompt)',
|
476 |
+
type: 'CLIPTextEncode',
|
477 |
+
},
|
478 |
+
name: 'text',
|
479 |
+
type: 'string',
|
480 |
+
value: 'text, watermark',
|
481 |
+
},
|
482 |
+
])
|
483 |
+
expect(
|
484 |
+
workflow.findInput({
|
485 |
+
name: 'text',
|
486 |
+
type: 'string',
|
487 |
+
nodeOutputToNodeInput: 'positive',
|
488 |
+
})
|
489 |
+
).toEqual([
|
490 |
+
{
|
491 |
+
id: '6.inputs.text',
|
492 |
+
node: {
|
493 |
+
id: '6',
|
494 |
+
name: 'CLIP Text Encode (Prompt)',
|
495 |
+
type: 'CLIPTextEncode',
|
496 |
+
},
|
497 |
+
name: 'text',
|
498 |
+
type: 'string',
|
499 |
+
value:
|
500 |
+
'beautiful scenery nature glass bottle landscape, , purple galaxy bottle,',
|
501 |
+
},
|
502 |
+
])
|
503 |
+
expect(
|
504 |
+
workflow.findInput({
|
505 |
+
name: /tex.*/,
|
506 |
+
type: /str.*/,
|
507 |
+
nodeOutputToNodeInput: /posi.*/,
|
508 |
+
})
|
509 |
+
).toEqual([
|
510 |
+
{
|
511 |
+
id: '6.inputs.text',
|
512 |
+
node: {
|
513 |
+
id: '6',
|
514 |
+
name: 'CLIP Text Encode (Prompt)',
|
515 |
+
type: 'CLIPTextEncode',
|
516 |
+
},
|
517 |
+
name: 'text',
|
518 |
+
type: 'string',
|
519 |
+
value:
|
520 |
+
'beautiful scenery nature glass bottle landscape, , purple galaxy bottle,',
|
521 |
+
},
|
522 |
+
])
|
523 |
+
})
|
524 |
+
|
525 |
+
test('should create the PromptBuilder', () => {
|
526 |
+
const promptBuilder = createPromptBuilder(
|
527 |
+
new ComfyUIWorkflowApiGraph(workflowRaw)
|
528 |
+
)
|
529 |
+
expect(promptBuilder.mapOutputKeys).toEqual({
|
530 |
+
[ClapperComfyUiInputIds.OUTPUT]: '9',
|
531 |
+
})
|
532 |
+
expect(promptBuilder.mapInputKeys).toEqual({
|
533 |
+
'3.inputs.seed': '3.inputs.seed',
|
534 |
+
'3.inputs.steps': '3.inputs.steps',
|
535 |
+
'3.inputs.cfg': '3.inputs.cfg',
|
536 |
+
'3.inputs.sampler_name': '3.inputs.sampler_name',
|
537 |
+
'3.inputs.scheduler': '3.inputs.scheduler',
|
538 |
+
'3.inputs.denoise': '3.inputs.denoise',
|
539 |
+
'4.inputs.ckpt_name': '4.inputs.ckpt_name',
|
540 |
+
'5.inputs.width': '5.inputs.width',
|
541 |
+
'5.inputs.height': '5.inputs.height',
|
542 |
+
'5.inputs.batch_size': '5.inputs.batch_size',
|
543 |
+
'7.inputs.text': '7.inputs.text',
|
544 |
+
'9.inputs.filename_prefix': '9.inputs.filename_prefix',
|
545 |
+
'6.inputs.text': '6.inputs.text',
|
546 |
+
[ClapperComfyUiInputIds.PROMPT]: ClapperComfyUiInputIds.PROMPT,
|
547 |
+
[ClapperComfyUiInputIds.NEGATIVE_PROMPT]:
|
548 |
+
ClapperComfyUiInputIds.NEGATIVE_PROMPT,
|
549 |
+
[ClapperComfyUiInputIds.WIDTH]: ClapperComfyUiInputIds.WIDTH,
|
550 |
+
[ClapperComfyUiInputIds.HEIGHT]: ClapperComfyUiInputIds.HEIGHT,
|
551 |
+
[ClapperComfyUiInputIds.SEED]: ClapperComfyUiInputIds.SEED,
|
552 |
+
})
|
553 |
+
expect(promptBuilder.prompt).toEqual(workflowRaw)
|
554 |
+
})
|
555 |
+
|
556 |
+
test('should convert correctly the workflow to string', () => {
|
557 |
+
const workflow = new ComfyUIWorkflowApiGraph(workflowRaw)
|
558 |
+
expect(workflow.toJson()).toEqual(workflowRaw)
|
559 |
+
})
|
560 |
+
|
561 |
+
test('should edit correctly an input of the workflow', () => {
|
562 |
+
const workflow = new ComfyUIWorkflowApiGraph(workflowRaw)
|
563 |
+
workflow.setInputValue('3.inputs.seed', 1121)
|
564 |
+
workflowRaw['3'].inputs.seed = 1111
|
565 |
+
expect(workflow.toJson()).not.toEqual(workflowRaw)
|
566 |
+
workflow.setInputValue('3.inputs.seed', 3333)
|
567 |
+
workflowRaw['3'].inputs.seed = 3333
|
568 |
+
expect(workflow.toJson()).toEqual(workflowRaw)
|
569 |
+
})
|
570 |
+
|
571 |
+
/**
|
572 |
+
* Error handling
|
573 |
+
*/
|
574 |
+
|
575 |
+
// TODO: More corrupted workflows
|
576 |
+
const workflowRawWithCycles = {
|
577 |
+
a: {
|
578 |
+
inputs: {
|
579 |
+
text: ['b', 0],
|
580 |
+
},
|
581 |
+
},
|
582 |
+
b: {
|
583 |
+
inputs: {
|
584 |
+
text: ['a', 0],
|
585 |
+
},
|
586 |
+
},
|
587 |
+
}
|
588 |
+
|
589 |
+
test('should fail if workflow has cycles', () => {
|
590 |
+
expect(() => {
|
591 |
+
new ComfyUIWorkflowApiGraph(workflowRawWithCycles)
|
592 |
+
}).toThrow(
|
593 |
+
'The provided workflow has cycles, impossible to get the output node.'
|
594 |
+
)
|
595 |
+
})
|
packages/app/src/app/api/resolve/providers/comfyui/createPromptBuilder.ts
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { PromptBuilder } from '@saintno/comfyui-sdk'
|
2 |
+
|
3 |
+
import { ClapperComfyUiInputIds } from './types'
|
4 |
+
import { ComfyUIWorkflowApiGraph } from './graph'
|
5 |
+
|
6 |
+
/**
|
7 |
+
* Takes a workflow graph and converts it to PromptBuilder
|
8 |
+
*/
|
9 |
+
export function createPromptBuilder(
|
10 |
+
workflowApiGraph: ComfyUIWorkflowApiGraph
|
11 |
+
): PromptBuilder<any, any, any> {
|
12 |
+
const inputs = workflowApiGraph.getInputs()
|
13 |
+
const outputNode = workflowApiGraph.getOutputNode()
|
14 |
+
|
15 |
+
const inputKeys = Object.values(inputs)
|
16 |
+
.map((input) => input.id)
|
17 |
+
.concat([
|
18 |
+
ClapperComfyUiInputIds.PROMPT,
|
19 |
+
ClapperComfyUiInputIds.NEGATIVE_PROMPT,
|
20 |
+
ClapperComfyUiInputIds.WIDTH,
|
21 |
+
ClapperComfyUiInputIds.HEIGHT,
|
22 |
+
ClapperComfyUiInputIds.SEED,
|
23 |
+
])
|
24 |
+
|
25 |
+
const promptBuilder = new PromptBuilder(
|
26 |
+
workflowApiGraph.toJson(),
|
27 |
+
inputKeys,
|
28 |
+
[ClapperComfyUiInputIds.OUTPUT]
|
29 |
+
)
|
30 |
+
|
31 |
+
// We don't need proper names for input keys,
|
32 |
+
// as we just use PromptBuilder for its websocket api
|
33 |
+
inputKeys.forEach((inputKey) => {
|
34 |
+
promptBuilder.setInputNode(inputKey, inputKey)
|
35 |
+
})
|
36 |
+
|
37 |
+
if (outputNode) {
|
38 |
+
promptBuilder.setOutputNode(ClapperComfyUiInputIds.OUTPUT, outputNode.id)
|
39 |
+
}
|
40 |
+
|
41 |
+
return promptBuilder
|
42 |
+
}
|
packages/app/src/app/api/resolve/providers/comfyui/getInputsFromComfyUiWorkflow.ts
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ClapInputCategory,
|
3 |
+
ClapInputField,
|
4 |
+
ClapInputFields,
|
5 |
+
ClapInputValues,
|
6 |
+
ClapWorkflowCategory,
|
7 |
+
} from '@aitube/clap'
|
8 |
+
|
9 |
+
import { MainClapWorkflowInputsLabels } from './utils'
|
10 |
+
import { getMainInputsFromComfyUiWorkflow } from './getMainInputsFromComfyUiWorkflow'
|
11 |
+
import { ComfyUIWorkflowApiGraph } from './graph'
|
12 |
+
|
13 |
+
/**
|
14 |
+
* Returns input fields / input values required by ComfyUi
|
15 |
+
* @param workflow
|
16 |
+
*/
|
17 |
+
export function getInputsFromComfyUiWorkflow(
|
18 |
+
workflowString: string,
|
19 |
+
category: ClapWorkflowCategory
|
20 |
+
): {
|
21 |
+
inputFields: ClapInputFields
|
22 |
+
inputValues: ClapInputValues
|
23 |
+
} {
|
24 |
+
const workflowGraph = ComfyUIWorkflowApiGraph.fromString(workflowString)
|
25 |
+
const { inputFields: mainInputFields, inputValues: mainInputValues } =
|
26 |
+
getMainInputsFromComfyUiWorkflow(workflowString, category)
|
27 |
+
|
28 |
+
const inputValues = {
|
29 |
+
...mainInputValues,
|
30 |
+
} as any
|
31 |
+
|
32 |
+
const inputFields: ClapInputField<{
|
33 |
+
options?: {
|
34 |
+
id: string
|
35 |
+
label: string
|
36 |
+
value: any
|
37 |
+
}[]
|
38 |
+
tooltip?: {
|
39 |
+
type: string
|
40 |
+
message: string
|
41 |
+
}
|
42 |
+
}>[] = [
|
43 |
+
// Required fields that should be available in the workflow, otherwise
|
44 |
+
// Clapper doesn't know how to input its settings (prompts, dimensions, etc)
|
45 |
+
{
|
46 |
+
id: '@clapper/mainInputs',
|
47 |
+
label: 'Main settings',
|
48 |
+
type: 'group' as any,
|
49 |
+
category: ClapInputCategory.UNKNOWN,
|
50 |
+
description: 'Main inputs',
|
51 |
+
defaultValue: '',
|
52 |
+
inputFields: mainInputFields,
|
53 |
+
},
|
54 |
+
// Other input fields based on the workflow nodes
|
55 |
+
{
|
56 |
+
id: '@clapper/otherInputs',
|
57 |
+
label: 'Node settings',
|
58 |
+
type: 'group' as any,
|
59 |
+
category: ClapInputCategory.UNKNOWN,
|
60 |
+
description: 'Main inputs',
|
61 |
+
defaultValue: '',
|
62 |
+
inputFields: workflowGraph
|
63 |
+
.getNodesWithInputs()
|
64 |
+
// Discard nodes with only inputs connections
|
65 |
+
.filter(({ id }) => workflowGraph.getInputsByNodeId(id)?.length)
|
66 |
+
.map(({ id, _meta }) => {
|
67 |
+
return {
|
68 |
+
id: '@clapper/node/' + id,
|
69 |
+
label: `${_meta?.title} (id: ${id})`,
|
70 |
+
type: 'group' as any,
|
71 |
+
category: ClapInputCategory.UNKNOWN,
|
72 |
+
description: `Settings for ${_meta?.title}`,
|
73 |
+
defaultValue: '',
|
74 |
+
inputFields: workflowGraph.getInputsByNodeId(id)?.map((input) => {
|
75 |
+
const mainInputKey = Object.keys(inputValues).find(
|
76 |
+
(key) => inputValues[key]?.id == input.id
|
77 |
+
)
|
78 |
+
inputValues[input.id] = input.value
|
79 |
+
return {
|
80 |
+
id: input.id,
|
81 |
+
label: input.name,
|
82 |
+
type: input.type as any,
|
83 |
+
category: ClapInputCategory.UNKNOWN,
|
84 |
+
description: '',
|
85 |
+
defaultValue: input.value,
|
86 |
+
metadata: {
|
87 |
+
tooltip: MainClapWorkflowInputsLabels[mainInputKey as string]
|
88 |
+
? {
|
89 |
+
type: 'warning',
|
90 |
+
message: `This value will be overwritten by Clapper because it is
|
91 |
+
used as "${MainClapWorkflowInputsLabels[mainInputKey as string]}".`,
|
92 |
+
}
|
93 |
+
: undefined,
|
94 |
+
},
|
95 |
+
}
|
96 |
+
}),
|
97 |
+
}
|
98 |
+
}),
|
99 |
+
},
|
100 |
+
]
|
101 |
+
|
102 |
+
return {
|
103 |
+
inputFields,
|
104 |
+
inputValues,
|
105 |
+
}
|
106 |
+
}
|
packages/app/src/app/api/resolve/providers/comfyui/getMainInputIdsByClapWorkflowCategory.ts
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ClapWorkflowCategory } from '@aitube/clap'
|
2 |
+
|
3 |
+
import { ClapperComfyUiInputIds } from './types'
|
4 |
+
|
5 |
+
export const getMainInputIdsByClapWorkflowCategory = (
|
6 |
+
category: ClapWorkflowCategory
|
7 |
+
) => {
|
8 |
+
switch (category) {
|
9 |
+
case ClapWorkflowCategory.VIDEO_GENERATION: {
|
10 |
+
return [
|
11 |
+
ClapperComfyUiInputIds.IMAGE,
|
12 |
+
ClapperComfyUiInputIds.WIDTH,
|
13 |
+
ClapperComfyUiInputIds.HEIGHT,
|
14 |
+
ClapperComfyUiInputIds.SEED,
|
15 |
+
ClapperComfyUiInputIds.OUTPUT,
|
16 |
+
]
|
17 |
+
}
|
18 |
+
default: {
|
19 |
+
return [
|
20 |
+
ClapperComfyUiInputIds.PROMPT,
|
21 |
+
ClapperComfyUiInputIds.NEGATIVE_PROMPT,
|
22 |
+
ClapperComfyUiInputIds.WIDTH,
|
23 |
+
ClapperComfyUiInputIds.HEIGHT,
|
24 |
+
ClapperComfyUiInputIds.SEED,
|
25 |
+
ClapperComfyUiInputIds.OUTPUT,
|
26 |
+
]
|
27 |
+
}
|
28 |
+
}
|
29 |
+
}
|
packages/app/src/app/api/resolve/providers/comfyui/getMainInputsFromComfyUiWorkflow.ts
ADDED
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ClapInputCategory,
|
3 |
+
ClapInputFields,
|
4 |
+
ClapInputValues,
|
5 |
+
ClapWorkflowCategory,
|
6 |
+
} from '@aitube/clap'
|
7 |
+
import unionBy from 'lodash/unionBy'
|
8 |
+
|
9 |
+
import {
|
10 |
+
findHeightInputsFromWorkflow,
|
11 |
+
findImageInputsFromWorkflow,
|
12 |
+
findNegativePromptInputsFromWorkflow,
|
13 |
+
findPromptInputsFromWorkflow,
|
14 |
+
findSeedInputsFromWorkflow,
|
15 |
+
findWidthInputsFromWorkflow,
|
16 |
+
MainClapWorkflowInputsLabels,
|
17 |
+
} from './utils'
|
18 |
+
import { ClapperComfyUiInputIds, ComfyUiWorkflowApiNodeInput } from './types'
|
19 |
+
import { ComfyUIWorkflowApiGraph } from './graph'
|
20 |
+
import { getMainInputIdsByClapWorkflowCategory } from './getMainInputIdsByClapWorkflowCategory'
|
21 |
+
|
22 |
+
export function getMainInputsFromComfyUiWorkflow(
|
23 |
+
workflowString: string,
|
24 |
+
category: ClapWorkflowCategory
|
25 |
+
): {
|
26 |
+
inputFields: ClapInputFields
|
27 |
+
inputValues: ClapInputValues
|
28 |
+
} {
|
29 |
+
const workflowGraph = ComfyUIWorkflowApiGraph.fromString(workflowString)
|
30 |
+
const mainInputsIds = getMainInputIdsByClapWorkflowCategory(category)
|
31 |
+
const nodes = workflowGraph.getNodes()
|
32 |
+
const textInputs = workflowGraph.findInput({
|
33 |
+
type: 'string',
|
34 |
+
name: /.*(text|prompt).*/,
|
35 |
+
})
|
36 |
+
const dimensionInputs = workflowGraph.findInput({
|
37 |
+
type: 'number',
|
38 |
+
name: /.*(width|height).*/,
|
39 |
+
})
|
40 |
+
const promptNodeInputs = findPromptInputsFromWorkflow(workflowGraph)
|
41 |
+
const negativePromptNodeInputs =
|
42 |
+
findNegativePromptInputsFromWorkflow(workflowGraph)
|
43 |
+
const widthNodeInputs = findWidthInputsFromWorkflow(workflowGraph)
|
44 |
+
const heightNodeInputs = findHeightInputsFromWorkflow(workflowGraph)
|
45 |
+
const seedNodeInputs = findSeedInputsFromWorkflow(workflowGraph)
|
46 |
+
const imageNodeInputs = findImageInputsFromWorkflow(workflowGraph)
|
47 |
+
const outputNode = workflowGraph.getOutputNode()
|
48 |
+
|
49 |
+
const inputValues = {
|
50 |
+
[ClapperComfyUiInputIds.PROMPT]: promptNodeInputs?.[0]
|
51 |
+
? {
|
52 |
+
id: promptNodeInputs?.[0].id,
|
53 |
+
label: `${promptNodeInputs?.[0].id} (from node ${promptNodeInputs?.[0].node.id})`,
|
54 |
+
}
|
55 |
+
: undefined,
|
56 |
+
[ClapperComfyUiInputIds.NEGATIVE_PROMPT]: negativePromptNodeInputs?.[0]
|
57 |
+
? {
|
58 |
+
id: negativePromptNodeInputs?.[0].id,
|
59 |
+
label: `${negativePromptNodeInputs?.[0].id} (from node ${negativePromptNodeInputs?.[0].node.id})`,
|
60 |
+
}
|
61 |
+
: undefined,
|
62 |
+
[ClapperComfyUiInputIds.WIDTH]: widthNodeInputs?.[0]
|
63 |
+
? {
|
64 |
+
id: widthNodeInputs?.[0].id,
|
65 |
+
label: `${widthNodeInputs?.[0].id} (from node ${widthNodeInputs?.[0].node.id})`,
|
66 |
+
}
|
67 |
+
: undefined,
|
68 |
+
[ClapperComfyUiInputIds.HEIGHT]: heightNodeInputs?.[0]
|
69 |
+
? {
|
70 |
+
id: heightNodeInputs?.[0].id,
|
71 |
+
label: `${heightNodeInputs?.[0].id} (from node ${heightNodeInputs?.[0].node.id})`,
|
72 |
+
}
|
73 |
+
: undefined,
|
74 |
+
[ClapperComfyUiInputIds.SEED]: seedNodeInputs?.[0]
|
75 |
+
? {
|
76 |
+
id: seedNodeInputs?.[0].id,
|
77 |
+
label: `${seedNodeInputs?.[0].id} (from node ${seedNodeInputs?.[0].node.id})`,
|
78 |
+
}
|
79 |
+
: undefined,
|
80 |
+
[ClapperComfyUiInputIds.IMAGE]: imageNodeInputs?.[0]
|
81 |
+
? {
|
82 |
+
id: imageNodeInputs?.[0].id,
|
83 |
+
label: `${imageNodeInputs?.[0].id} (from node ${imageNodeInputs?.[0].node.id})`,
|
84 |
+
}
|
85 |
+
: undefined,
|
86 |
+
[ClapperComfyUiInputIds.OUTPUT]: outputNode
|
87 |
+
? {
|
88 |
+
id: outputNode?.id,
|
89 |
+
label: `${outputNode?._meta?.title} (id: ${outputNode?.id})`,
|
90 |
+
}
|
91 |
+
: undefined,
|
92 |
+
}
|
93 |
+
|
94 |
+
const getOptionsItems = (inputs: ComfyUiWorkflowApiNodeInput[]) => {
|
95 |
+
return [
|
96 |
+
...inputs,
|
97 |
+
{
|
98 |
+
id: ClapperComfyUiInputIds.NULL,
|
99 |
+
name: 'Unset',
|
100 |
+
node: {
|
101 |
+
id: null,
|
102 |
+
},
|
103 |
+
},
|
104 |
+
].map((p) => {
|
105 |
+
const item = {
|
106 |
+
id: p.id,
|
107 |
+
label:
|
108 |
+
p.id === ClapperComfyUiInputIds.NULL
|
109 |
+
? `Unset`
|
110 |
+
: `${p.name} (from node ${p.node.id})`,
|
111 |
+
}
|
112 |
+
return {
|
113 |
+
...item,
|
114 |
+
value: item,
|
115 |
+
}
|
116 |
+
})
|
117 |
+
}
|
118 |
+
|
119 |
+
const inputFields: any = []
|
120 |
+
mainInputsIds.forEach((mainInput) => {
|
121 |
+
switch (mainInput) {
|
122 |
+
case ClapperComfyUiInputIds.PROMPT: {
|
123 |
+
inputFields.push({
|
124 |
+
id: ClapperComfyUiInputIds.PROMPT,
|
125 |
+
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.PROMPT],
|
126 |
+
type: 'nodeInput' as any,
|
127 |
+
category: ClapInputCategory.PROMPT,
|
128 |
+
description: 'The input where Clapper will put the segment prompt',
|
129 |
+
defaultValue: '',
|
130 |
+
metadata: {
|
131 |
+
options: getOptionsItems(
|
132 |
+
unionBy(promptNodeInputs, textInputs, 'id')
|
133 |
+
),
|
134 |
+
},
|
135 |
+
})
|
136 |
+
return
|
137 |
+
}
|
138 |
+
case ClapperComfyUiInputIds.NEGATIVE_PROMPT: {
|
139 |
+
inputFields.push({
|
140 |
+
id: ClapperComfyUiInputIds.NEGATIVE_PROMPT,
|
141 |
+
label:
|
142 |
+
MainClapWorkflowInputsLabels[
|
143 |
+
ClapperComfyUiInputIds.NEGATIVE_PROMPT
|
144 |
+
],
|
145 |
+
type: 'nodeInput' as any,
|
146 |
+
category: ClapInputCategory.PROMPT,
|
147 |
+
description:
|
148 |
+
'The node input where Clapper will put the segment negative prompt',
|
149 |
+
defaultValue: '',
|
150 |
+
metadata: {
|
151 |
+
options: getOptionsItems(
|
152 |
+
unionBy(negativePromptNodeInputs, textInputs, 'id')
|
153 |
+
),
|
154 |
+
},
|
155 |
+
})
|
156 |
+
return
|
157 |
+
}
|
158 |
+
case ClapperComfyUiInputIds.WIDTH: {
|
159 |
+
inputFields.push({
|
160 |
+
id: ClapperComfyUiInputIds.WIDTH,
|
161 |
+
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.WIDTH],
|
162 |
+
type: 'nodeInput' as any,
|
163 |
+
category: ClapInputCategory.WIDTH,
|
164 |
+
description:
|
165 |
+
'The node input where Clapper will put the required image width',
|
166 |
+
defaultValue: '',
|
167 |
+
metadata: {
|
168 |
+
options: getOptionsItems(
|
169 |
+
unionBy(widthNodeInputs, dimensionInputs, 'id')
|
170 |
+
),
|
171 |
+
},
|
172 |
+
})
|
173 |
+
return
|
174 |
+
}
|
175 |
+
case ClapperComfyUiInputIds.HEIGHT: {
|
176 |
+
inputFields.push({
|
177 |
+
id: ClapperComfyUiInputIds.HEIGHT,
|
178 |
+
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.HEIGHT],
|
179 |
+
type: 'nodeInput' as any,
|
180 |
+
description:
|
181 |
+
'The node input where Clapper will put the required image height',
|
182 |
+
category: ClapInputCategory.HEIGHT,
|
183 |
+
defaultValue: 1000,
|
184 |
+
metadata: {
|
185 |
+
options: getOptionsItems(
|
186 |
+
unionBy(heightNodeInputs, dimensionInputs, 'id')
|
187 |
+
),
|
188 |
+
},
|
189 |
+
})
|
190 |
+
return
|
191 |
+
}
|
192 |
+
case ClapperComfyUiInputIds.SEED: {
|
193 |
+
inputFields.push({
|
194 |
+
id: ClapperComfyUiInputIds.SEED,
|
195 |
+
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.SEED],
|
196 |
+
type: 'nodeInput' as any,
|
197 |
+
category: ClapInputCategory.UNKNOWN,
|
198 |
+
description: 'The node input where Clapper will set a seed',
|
199 |
+
defaultValue: '',
|
200 |
+
metadata: {
|
201 |
+
options: getOptionsItems(seedNodeInputs),
|
202 |
+
},
|
203 |
+
})
|
204 |
+
return
|
205 |
+
}
|
206 |
+
case ClapperComfyUiInputIds.IMAGE: {
|
207 |
+
inputFields.push({
|
208 |
+
id: ClapperComfyUiInputIds.IMAGE,
|
209 |
+
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.IMAGE],
|
210 |
+
type: 'nodeInput' as any,
|
211 |
+
category: ClapInputCategory.UNKNOWN,
|
212 |
+
description: 'The node input where Clapper will set an image',
|
213 |
+
defaultValue: '',
|
214 |
+
metadata: {
|
215 |
+
options: getOptionsItems(imageNodeInputs),
|
216 |
+
tooltip: {
|
217 |
+
message: `
|
218 |
+
Clapper doesn't support file/upload node inputs;
|
219 |
+
use a string input from where Clapper can load a base64
|
220 |
+
data URI string (e.g. the "Load Image From Base64" node's
|
221 |
+
"data" input in the default video workflow).
|
222 |
+
`,
|
223 |
+
type: 'info',
|
224 |
+
},
|
225 |
+
},
|
226 |
+
})
|
227 |
+
return
|
228 |
+
}
|
229 |
+
case ClapperComfyUiInputIds.OUTPUT: {
|
230 |
+
inputFields.push({
|
231 |
+
id: ClapperComfyUiInputIds.OUTPUT,
|
232 |
+
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.OUTPUT],
|
233 |
+
type: 'node' as any,
|
234 |
+
category: ClapInputCategory.UNKNOWN,
|
235 |
+
description: 'The node from which Clapper will get the output image',
|
236 |
+
defaultValue: '',
|
237 |
+
metadata: {
|
238 |
+
options: nodes.map((p) => {
|
239 |
+
const item = {
|
240 |
+
id: p.id,
|
241 |
+
label: `${p._meta?.title || 'Untitled node'} (id: ${p.id})`,
|
242 |
+
}
|
243 |
+
return {
|
244 |
+
...item,
|
245 |
+
value: item,
|
246 |
+
}
|
247 |
+
}),
|
248 |
+
},
|
249 |
+
})
|
250 |
+
return
|
251 |
+
}
|
252 |
+
}
|
253 |
+
})
|
254 |
+
|
255 |
+
return {
|
256 |
+
inputFields,
|
257 |
+
inputValues,
|
258 |
+
}
|
259 |
+
}
|
packages/app/src/app/api/resolve/providers/comfyui/graph.ts
ADDED
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ComfyUIWorkflowApiJson,
|
3 |
+
ComfyUiWorkflowApiNodeInput,
|
4 |
+
INPUT_TYPES,
|
5 |
+
NodeData,
|
6 |
+
} from './types'
|
7 |
+
|
8 |
+
export class ComfyUIWorkflowApiGraphEdge {
|
9 |
+
source: ComfyUIWorkflowApiGraphNode
|
10 |
+
target: ComfyUIWorkflowApiGraphNode
|
11 |
+
relationship: string
|
12 |
+
metadata?: any
|
13 |
+
|
14 |
+
constructor(
|
15 |
+
source: ComfyUIWorkflowApiGraphNode,
|
16 |
+
target: ComfyUIWorkflowApiGraphNode,
|
17 |
+
relationship: string,
|
18 |
+
metadata?: any
|
19 |
+
) {
|
20 |
+
this.source = source
|
21 |
+
this.target = target
|
22 |
+
this.relationship = relationship
|
23 |
+
this.metadata = metadata
|
24 |
+
}
|
25 |
+
}
|
26 |
+
|
27 |
+
export class ComfyUIWorkflowApiGraphNode {
|
28 |
+
id: string
|
29 |
+
inputs?: Record<string, any> = {}
|
30 |
+
_meta?: Record<string, any> = {}
|
31 |
+
class_type?: string = ''
|
32 |
+
outboundEdges: ComfyUIWorkflowApiGraphEdge[] = []
|
33 |
+
inboundEdges: ComfyUIWorkflowApiGraphEdge[] = []
|
34 |
+
|
35 |
+
constructor(id: string, inputs?: Record<string, any>, meta?: any) {
|
36 |
+
this.id = id
|
37 |
+
}
|
38 |
+
|
39 |
+
addOutboundEdge(
|
40 |
+
targetNode: ComfyUIWorkflowApiGraphNode,
|
41 |
+
relationship: string,
|
42 |
+
metadata?: any
|
43 |
+
) {
|
44 |
+
const edge = new ComfyUIWorkflowApiGraphEdge(
|
45 |
+
this,
|
46 |
+
targetNode,
|
47 |
+
relationship,
|
48 |
+
metadata
|
49 |
+
)
|
50 |
+
this.outboundEdges.push(edge)
|
51 |
+
targetNode.inboundEdges.push(edge)
|
52 |
+
}
|
53 |
+
|
54 |
+
addInboundEdge(
|
55 |
+
sourceNode: ComfyUIWorkflowApiGraphNode,
|
56 |
+
relationship: string,
|
57 |
+
metadata?: any
|
58 |
+
) {
|
59 |
+
const edge = new ComfyUIWorkflowApiGraphEdge(
|
60 |
+
sourceNode,
|
61 |
+
this,
|
62 |
+
relationship,
|
63 |
+
metadata
|
64 |
+
)
|
65 |
+
this.inboundEdges.push(edge)
|
66 |
+
sourceNode.outboundEdges.push(edge)
|
67 |
+
}
|
68 |
+
|
69 |
+
getOutboundEdges(): ComfyUIWorkflowApiGraphEdge[] {
|
70 |
+
return this.outboundEdges
|
71 |
+
}
|
72 |
+
|
73 |
+
getInboundEdges(): ComfyUIWorkflowApiGraphEdge[] {
|
74 |
+
return this.inboundEdges
|
75 |
+
}
|
76 |
+
|
77 |
+
toJson(): Record<string, any> {
|
78 |
+
return structuredClone({
|
79 |
+
inputs: this.inputs,
|
80 |
+
class_type: this.class_type,
|
81 |
+
_meta: this._meta,
|
82 |
+
})
|
83 |
+
}
|
84 |
+
}
|
85 |
+
|
86 |
+
/**
|
87 |
+
* Utils to query ComfyUI workflow-api nodes data
|
88 |
+
*/
|
89 |
+
export class ComfyUIWorkflowApiGraph {
|
90 |
+
private json: ComfyUIWorkflowApiJson
|
91 |
+
private nodes: Record<string, ComfyUIWorkflowApiGraphNode> = {}
|
92 |
+
private adjList: Record<string, string[]> = {}
|
93 |
+
private dependencyList: Record<
|
94 |
+
string,
|
95 |
+
{ from: string; inputName: string }[]
|
96 |
+
> = {}
|
97 |
+
private dependantList: Record<string, { to: string; inputName: string }[]> =
|
98 |
+
{}
|
99 |
+
private inDegree: Record<string, number> = {}
|
100 |
+
|
101 |
+
constructor(workflow: ComfyUIWorkflowApiJson) {
|
102 |
+
this.json = structuredClone(workflow)
|
103 |
+
const { adjList, dependencyList, dependantList, inDegree } =
|
104 |
+
this.buildGraphData()
|
105 |
+
this.adjList = adjList
|
106 |
+
this.dependencyList = dependencyList
|
107 |
+
this.dependantList = dependantList
|
108 |
+
this.inDegree = inDegree
|
109 |
+
const hasCycles = this.detectCycles()
|
110 |
+
if (hasCycles) {
|
111 |
+
throw new Error(
|
112 |
+
'The provided workflow has cycles, impossible to get the output node.'
|
113 |
+
)
|
114 |
+
}
|
115 |
+
}
|
116 |
+
|
117 |
+
/**
|
118 |
+
* Create a graph structure in an adjacent list
|
119 |
+
* representation with additional data arrays
|
120 |
+
* for dev purposes.
|
121 |
+
* @param workflow
|
122 |
+
* @returns
|
123 |
+
*/
|
124 |
+
private buildGraphData() {
|
125 |
+
const adjList: Record<string, string[]> = {}
|
126 |
+
const dependencyList: Record<
|
127 |
+
string,
|
128 |
+
{ from: string; inputName: string }[]
|
129 |
+
> = {}
|
130 |
+
const dependantList: Record<string, { to: string; inputName: string }[]> =
|
131 |
+
{}
|
132 |
+
const inDegree: Record<string, number> = {}
|
133 |
+
|
134 |
+
for (const nodeId of Object.keys(this.json)) {
|
135 |
+
const node = new ComfyUIWorkflowApiGraphNode(nodeId)
|
136 |
+
node.inputs = this.json[nodeId].inputs
|
137 |
+
node._meta = this.json[nodeId]._meta
|
138 |
+
node.class_type = this.json[nodeId].class_type
|
139 |
+
this.nodes[nodeId] = node
|
140 |
+
|
141 |
+
adjList[nodeId] = []
|
142 |
+
dependencyList[nodeId] = []
|
143 |
+
dependantList[nodeId] = []
|
144 |
+
inDegree[nodeId] = 0
|
145 |
+
}
|
146 |
+
|
147 |
+
for (const node of Object.values(this.nodes)) {
|
148 |
+
if (node.inputs) {
|
149 |
+
for (const [inputName, value] of Object.entries(node.inputs)) {
|
150 |
+
if (Array.isArray(value)) {
|
151 |
+
const dependencyNodeId = value[0] as string
|
152 |
+
adjList[dependencyNodeId].push(node.id)
|
153 |
+
dependencyList[node.id].push({ from: dependencyNodeId, inputName })
|
154 |
+
node.addInboundEdge(this.nodes[dependencyNodeId], inputName)
|
155 |
+
dependantList[dependencyNodeId].push({ to: node.id, inputName })
|
156 |
+
inDegree[node.id] += 1
|
157 |
+
}
|
158 |
+
}
|
159 |
+
}
|
160 |
+
}
|
161 |
+
|
162 |
+
return { adjList, dependencyList, dependantList, inDegree }
|
163 |
+
}
|
164 |
+
|
165 |
+
/**
|
166 |
+
* Check for corrupted workflows with loops
|
167 |
+
*/
|
168 |
+
private detectCycles(): boolean {
|
169 |
+
const visited: Record<string, boolean> = {}
|
170 |
+
const recursionStack: Record<string, boolean> = {}
|
171 |
+
const dfs = (nodeId: string): boolean => {
|
172 |
+
if (!visited[nodeId]) {
|
173 |
+
visited[nodeId] = true
|
174 |
+
recursionStack[nodeId] = true
|
175 |
+
for (const neighbor of this.adjList[nodeId]) {
|
176 |
+
if (!visited[neighbor] && dfs(neighbor)) {
|
177 |
+
return true
|
178 |
+
} else if (recursionStack[neighbor]) {
|
179 |
+
return true
|
180 |
+
}
|
181 |
+
}
|
182 |
+
}
|
183 |
+
|
184 |
+
recursionStack[nodeId] = false
|
185 |
+
return false
|
186 |
+
}
|
187 |
+
|
188 |
+
for (const nodeId of Object.keys(this.adjList)) {
|
189 |
+
if (dfs(nodeId)) {
|
190 |
+
return true
|
191 |
+
}
|
192 |
+
}
|
193 |
+
|
194 |
+
return false
|
195 |
+
}
|
196 |
+
|
197 |
+
getGraphData() {
|
198 |
+
const { adjList, dependencyList, dependantList, inDegree } = this
|
199 |
+
return { adjList, dependencyList, dependantList, inDegree }
|
200 |
+
}
|
201 |
+
|
202 |
+
/**
|
203 |
+
* Get all nodes that have inputs.
|
204 |
+
*/
|
205 |
+
getNodesWithInputs(): ComfyUIWorkflowApiGraphNode[] {
|
206 |
+
const nodesWithInputs: ComfyUIWorkflowApiGraphNode[] = []
|
207 |
+
for (const node of Object.values(this.nodes)) {
|
208 |
+
if (node.inputs && Object.keys(node.inputs).length > 0) {
|
209 |
+
nodesWithInputs.push(node)
|
210 |
+
}
|
211 |
+
}
|
212 |
+
return nodesWithInputs
|
213 |
+
}
|
214 |
+
|
215 |
+
getNodes(): ComfyUIWorkflowApiGraphNode[] {
|
216 |
+
return Object.values(structuredClone(this.nodes))
|
217 |
+
}
|
218 |
+
|
219 |
+
getNodesDict(): Record<string, ComfyUIWorkflowApiGraphNode> {
|
220 |
+
return structuredClone(this.nodes)
|
221 |
+
}
|
222 |
+
|
223 |
+
/**
|
224 |
+
* Returns a copy of all inputs in the workflow.
|
225 |
+
*/
|
226 |
+
getInputs(): Record<string, ComfyUiWorkflowApiNodeInput> {
|
227 |
+
const nodesWithInputs = this.getNodesWithInputs()
|
228 |
+
const inputs = {}
|
229 |
+
|
230 |
+
for (const node of nodesWithInputs) {
|
231 |
+
const inputSchemas = this.getInputsByNodeId(node.id)
|
232 |
+
inputSchemas?.forEach((inputSchema) => {
|
233 |
+
inputs[`${inputSchema.node.id}.inputs.${inputSchema.name}`] =
|
234 |
+
inputSchema
|
235 |
+
})
|
236 |
+
}
|
237 |
+
|
238 |
+
return inputs
|
239 |
+
}
|
240 |
+
|
241 |
+
/**
|
242 |
+
* Topological sort of the graph (Kahn's Algorithm) to get the final output node.
|
243 |
+
* TODO: multiple outputs.
|
244 |
+
*/
|
245 |
+
getOutputNode(): NodeData | null {
|
246 |
+
const { adjList, inDegree } = this
|
247 |
+
const queue: string[] = []
|
248 |
+
const sortedOrder: string[] = []
|
249 |
+
|
250 |
+
for (const nodeId of Object.keys(inDegree)) {
|
251 |
+
if (inDegree[nodeId] === 0) {
|
252 |
+
queue.push(nodeId)
|
253 |
+
}
|
254 |
+
}
|
255 |
+
|
256 |
+
while (queue.length > 0) {
|
257 |
+
const currentNode = queue.shift()!
|
258 |
+
sortedOrder.push(currentNode)
|
259 |
+
for (const neighbor of adjList[currentNode]) {
|
260 |
+
inDegree[neighbor] -= 1
|
261 |
+
if (inDegree[neighbor] === 0) {
|
262 |
+
queue.push(neighbor)
|
263 |
+
}
|
264 |
+
}
|
265 |
+
}
|
266 |
+
|
267 |
+
// Last node in sortedOrder is the output node
|
268 |
+
// TODO: handle multiple outputs
|
269 |
+
if (sortedOrder.length === Object.keys(this.json).length) {
|
270 |
+
const outputNodeId = sortedOrder[sortedOrder.length - 1]
|
271 |
+
return { id: outputNodeId, ...this.json[outputNodeId] }
|
272 |
+
} else {
|
273 |
+
// If there are cycles, fail
|
274 |
+
throw new Error(
|
275 |
+
'The provided workflow has cycles, impossible to get the output node.'
|
276 |
+
)
|
277 |
+
}
|
278 |
+
}
|
279 |
+
|
280 |
+
/**
|
281 |
+
* Get all value inputs of a given node in the workflow.
|
282 |
+
* Ignore input connections (e.g. inputs with value ['3', 0])
|
283 |
+
* @param nodeId the id of the node
|
284 |
+
*/
|
285 |
+
getInputsByNodeId(nodeId: string): ComfyUiWorkflowApiNodeInput[] | null {
|
286 |
+
const nodeData = this.nodes[nodeId]
|
287 |
+
if (!nodeData || !nodeData.inputs) {
|
288 |
+
return null
|
289 |
+
}
|
290 |
+
|
291 |
+
const inputs: ComfyUiWorkflowApiNodeInput[] = []
|
292 |
+
|
293 |
+
for (const [name, value] of Object.entries(nodeData.inputs)) {
|
294 |
+
if (Array.isArray(value)) continue
|
295 |
+
|
296 |
+
// TODO: Handle more types
|
297 |
+
let inputType: INPUT_TYPES =
|
298 |
+
typeof value === 'string' ? 'string' : 'number'
|
299 |
+
|
300 |
+
inputs.push({
|
301 |
+
type: inputType,
|
302 |
+
name: name,
|
303 |
+
value: value,
|
304 |
+
id: `${nodeId}.inputs.${name}`,
|
305 |
+
node: {
|
306 |
+
id: nodeId,
|
307 |
+
type: nodeData.class_type,
|
308 |
+
name: nodeData?._meta?.title,
|
309 |
+
},
|
310 |
+
})
|
311 |
+
}
|
312 |
+
|
313 |
+
return inputs.length > 0 ? inputs : null
|
314 |
+
}
|
315 |
+
|
316 |
+
/**
|
317 |
+
* A simple search of workflow inputs
|
318 |
+
* @param query
|
319 |
+
*/
|
320 |
+
findInput(query: {
|
321 |
+
// By type of node input
|
322 |
+
type?: string | RegExp
|
323 |
+
// By name of node input
|
324 |
+
name?: string | RegExp
|
325 |
+
// Based on the node
|
326 |
+
nodeType?: string | RegExp
|
327 |
+
nodeName?: string | RegExp
|
328 |
+
// If any output of the node containing the input
|
329 |
+
// is targeting another node's input with the given
|
330 |
+
// name
|
331 |
+
nodeOutputToNodeInput?: string | RegExp
|
332 |
+
// By value
|
333 |
+
value?: (value) => boolean
|
334 |
+
}): ComfyUiWorkflowApiNodeInput[] {
|
335 |
+
const inputs = this.getInputs()
|
336 |
+
|
337 |
+
// Helper function to match string or RegExp
|
338 |
+
const matches = (
|
339 |
+
value: string | undefined,
|
340 |
+
query: string | RegExp | undefined
|
341 |
+
): boolean => {
|
342 |
+
if (!query) return true
|
343 |
+
if (typeof query === 'string') {
|
344 |
+
return value === query
|
345 |
+
} else if (query instanceof RegExp) {
|
346 |
+
return query.test(value || '')
|
347 |
+
}
|
348 |
+
return false
|
349 |
+
}
|
350 |
+
|
351 |
+
return Object.values(inputs).filter(
|
352 |
+
(input) =>
|
353 |
+
(matches(input.type, query.type) &&
|
354 |
+
matches(input.name, query.name) &&
|
355 |
+
matches(input.node.name, query.nodeName) &&
|
356 |
+
matches(input.node.type, query.nodeType) &&
|
357 |
+
(!query.nodeOutputToNodeInput ||
|
358 |
+
this.nodes[input.node.id].outboundEdges.some((edge) =>
|
359 |
+
matches(edge.relationship, query.nodeOutputToNodeInput)
|
360 |
+
)) &&
|
361 |
+
!query.value) ||
|
362 |
+
query.value?.(input.value)
|
363 |
+
)
|
364 |
+
}
|
365 |
+
|
366 |
+
setInputValue(
|
367 |
+
inputKey: string,
|
368 |
+
value: any,
|
369 |
+
options?: {
|
370 |
+
ignoreErrors?: boolean
|
371 |
+
}
|
372 |
+
) {
|
373 |
+
const inputs = this.getInputs()
|
374 |
+
if (!options?.ignoreErrors && !inputs[inputKey]) {
|
375 |
+
throw new Error("Input doesn't exist in the workflow")
|
376 |
+
}
|
377 |
+
const input = inputs[inputKey]
|
378 |
+
if (!input) return
|
379 |
+
if (!this.nodes[input.node.id].inputs) this.nodes[input.node.id].inputs = {}
|
380 |
+
this.nodes[input.node.id].inputs![input.name] = value
|
381 |
+
}
|
382 |
+
|
383 |
+
toJson(): Record<string, any> {
|
384 |
+
const nodes = this.nodes
|
385 |
+
const nodesJson = {}
|
386 |
+
for (const key of Object.keys(nodes)) {
|
387 |
+
nodesJson[key] = nodes[key].toJson()
|
388 |
+
}
|
389 |
+
return nodesJson
|
390 |
+
}
|
391 |
+
|
392 |
+
toString() {
|
393 |
+
return JSON.stringify(this.toJson())
|
394 |
+
}
|
395 |
+
|
396 |
+
static fromString(
|
397 |
+
workflowString: string | undefined
|
398 |
+
): ComfyUIWorkflowApiGraph {
|
399 |
+
if (!workflowString) throw new Error('Invalid workflow.')
|
400 |
+
const workflowRaw = JSON.parse(workflowString)
|
401 |
+
return new ComfyUIWorkflowApiGraph(workflowRaw)
|
402 |
+
}
|
403 |
+
|
404 |
+
static isValidWorkflow(workflowString: string | undefined): boolean {
|
405 |
+
try {
|
406 |
+
ComfyUIWorkflowApiGraph.fromString(workflowString)
|
407 |
+
return true
|
408 |
+
} catch {}
|
409 |
+
return false
|
410 |
+
}
|
411 |
+
}
|
packages/app/src/app/api/resolve/providers/comfyui/index.ts
CHANGED
@@ -5,15 +5,16 @@ import {
|
|
5 |
ClapWorkflowCategory,
|
6 |
generateSeed,
|
7 |
} from '@aitube/clap'
|
|
|
|
|
8 |
import { TimelineSegment } from '@aitube/timeline'
|
|
|
9 |
import { BasicCredentials, CallWrapper, ComfyApi } from '@saintno/comfyui-sdk'
|
|
|
10 |
import { decodeOutput } from '@/lib/utils/decodeOutput'
|
11 |
-
import {
|
12 |
-
|
13 |
-
|
14 |
-
createPromptBuilder,
|
15 |
-
} from './utils'
|
16 |
-
import { ClapInputValueObject } from '@aitube/clap/dist/types'
|
17 |
|
18 |
export async function resolveSegment(
|
19 |
request: ResolveRequest
|
|
|
5 |
ClapWorkflowCategory,
|
6 |
generateSeed,
|
7 |
} from '@aitube/clap'
|
8 |
+
import { ClapInputValueObject } from '@aitube/clap/dist/types'
|
9 |
+
|
10 |
import { TimelineSegment } from '@aitube/timeline'
|
11 |
+
|
12 |
import { BasicCredentials, CallWrapper, ComfyApi } from '@saintno/comfyui-sdk'
|
13 |
+
|
14 |
import { decodeOutput } from '@/lib/utils/decodeOutput'
|
15 |
+
import { ClapperComfyUiInputIds } from './types'
|
16 |
+
import { createPromptBuilder } from './createPromptBuilder'
|
17 |
+
import { ComfyUIWorkflowApiGraph } from './graph'
|
|
|
|
|
|
|
18 |
|
19 |
export async function resolveSegment(
|
20 |
request: ResolveRequest
|
packages/app/src/app/api/resolve/providers/comfyui/{utils.spec.ts → tests.spec.ts}
RENAMED
@@ -1,11 +1,11 @@
|
|
1 |
import { expect, test } from 'vitest'
|
2 |
import {
|
3 |
-
ClapperComfyUiInputIds,
|
4 |
-
ComfyUIWorkflowApiGraph,
|
5 |
-
createPromptBuilder,
|
6 |
findNegativePromptInputsFromWorkflow,
|
7 |
findPromptInputsFromWorkflow,
|
8 |
} from './utils'
|
|
|
|
|
|
|
9 |
|
10 |
// Default workflow used by ComfyUI, downloaded for API
|
11 |
const workflowRaw = {
|
|
|
1 |
import { expect, test } from 'vitest'
|
2 |
import {
|
|
|
|
|
|
|
3 |
findNegativePromptInputsFromWorkflow,
|
4 |
findPromptInputsFromWorkflow,
|
5 |
} from './utils'
|
6 |
+
import { ComfyUIWorkflowApiGraph } from './graph'
|
7 |
+
import { createPromptBuilder } from './createPromptBuilder'
|
8 |
+
import { ClapperComfyUiInputIds } from './types'
|
9 |
|
10 |
// Default workflow used by ComfyUI, downloaded for API
|
11 |
const workflowRaw = {
|
packages/app/src/app/api/resolve/providers/comfyui/types.ts
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export enum ClapperComfyUiInputIds {
|
2 |
+
PROMPT = '@clapper/prompt',
|
3 |
+
NEGATIVE_PROMPT = '@clapper/negative/prompt',
|
4 |
+
WIDTH = '@clapper/width',
|
5 |
+
HEIGHT = '@clapper/height',
|
6 |
+
SEED = '@clapper/seed',
|
7 |
+
IMAGE = '@clapper/image',
|
8 |
+
OUTPUT = '@clapper/output',
|
9 |
+
NULL = '@clapper/null',
|
10 |
+
}
|
11 |
+
|
12 |
+
export type NodeRawData = {
|
13 |
+
inputs?: Record<string, unknown>
|
14 |
+
class_type?: string
|
15 |
+
_meta?: {
|
16 |
+
title: string
|
17 |
+
}
|
18 |
+
}
|
19 |
+
|
20 |
+
export type NodeData = NodeRawData & {
|
21 |
+
id: string
|
22 |
+
}
|
23 |
+
|
24 |
+
export type ComfyUIWorkflowApiJson = Record<string, NodeRawData>
|
25 |
+
|
26 |
+
export type INPUT_TYPES = 'string' | 'number'
|
27 |
+
|
28 |
+
export type ComfyUiWorkflowApiNodeInput = {
|
29 |
+
id: string
|
30 |
+
// Infered primitive type of the input based on its value
|
31 |
+
type: INPUT_TYPES
|
32 |
+
name: string
|
33 |
+
value: any
|
34 |
+
node: {
|
35 |
+
id: string
|
36 |
+
name?: string
|
37 |
+
type?: string
|
38 |
+
}
|
39 |
+
}
|
packages/app/src/app/api/resolve/providers/comfyui/utils.ts
CHANGED
@@ -1,460 +1,7 @@
|
|
1 |
-
import {
|
2 |
-
ClapInputCategory,
|
3 |
-
ClapInputField,
|
4 |
-
ClapInputFields,
|
5 |
-
ClapInputValues,
|
6 |
-
ClapWorkflow,
|
7 |
-
ClapWorkflowCategory,
|
8 |
-
ClapWorkflowEngine,
|
9 |
-
ClapWorkflowProvider,
|
10 |
-
} from '@aitube/clap'
|
11 |
-
import { PromptBuilder } from '@saintno/comfyui-sdk'
|
12 |
import unionBy from 'lodash/unionBy'
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
NEGATIVE_PROMPT = '@clapper/negative/prompt',
|
17 |
-
WIDTH = '@clapper/width',
|
18 |
-
HEIGHT = '@clapper/height',
|
19 |
-
SEED = '@clapper/seed',
|
20 |
-
IMAGE = '@clapper/image',
|
21 |
-
OUTPUT = '@clapper/output',
|
22 |
-
NULL = '@clapper/null',
|
23 |
-
}
|
24 |
-
|
25 |
-
type NodeRawData = {
|
26 |
-
inputs?: Record<string, unknown>
|
27 |
-
class_type?: string
|
28 |
-
_meta?: {
|
29 |
-
title: string
|
30 |
-
}
|
31 |
-
}
|
32 |
-
|
33 |
-
type NodeData = NodeRawData & {
|
34 |
-
id: string
|
35 |
-
}
|
36 |
-
|
37 |
-
type ComfyUIWorkflowApiJson = Record<string, NodeRawData>
|
38 |
-
|
39 |
-
type INPUT_TYPES = 'string' | 'number'
|
40 |
-
|
41 |
-
export type ComfyUiWorkflowApiNodeInput = {
|
42 |
-
id: string
|
43 |
-
// Infered primitive type of the input based on its value
|
44 |
-
type: INPUT_TYPES
|
45 |
-
name: string
|
46 |
-
value: any
|
47 |
-
node: {
|
48 |
-
id: string
|
49 |
-
name?: string
|
50 |
-
type?: string
|
51 |
-
}
|
52 |
-
}
|
53 |
-
|
54 |
-
export class ComfyUIWorkflowApiGraphEdge {
|
55 |
-
source: ComfyUIWorkflowApiGraphNode
|
56 |
-
target: ComfyUIWorkflowApiGraphNode
|
57 |
-
relationship: string
|
58 |
-
metadata?: any
|
59 |
-
|
60 |
-
constructor(
|
61 |
-
source: ComfyUIWorkflowApiGraphNode,
|
62 |
-
target: ComfyUIWorkflowApiGraphNode,
|
63 |
-
relationship: string,
|
64 |
-
metadata?: any
|
65 |
-
) {
|
66 |
-
this.source = source
|
67 |
-
this.target = target
|
68 |
-
this.relationship = relationship
|
69 |
-
this.metadata = metadata
|
70 |
-
}
|
71 |
-
}
|
72 |
-
|
73 |
-
export class ComfyUIWorkflowApiGraphNode {
|
74 |
-
id: string
|
75 |
-
inputs?: Record<string, any> = {}
|
76 |
-
_meta?: Record<string, any> = {}
|
77 |
-
class_type?: string = ''
|
78 |
-
outboundEdges: ComfyUIWorkflowApiGraphEdge[] = []
|
79 |
-
inboundEdges: ComfyUIWorkflowApiGraphEdge[] = []
|
80 |
-
|
81 |
-
constructor(id: string, inputs?: Record<string, any>, meta?: any) {
|
82 |
-
this.id = id
|
83 |
-
}
|
84 |
-
|
85 |
-
addOutboundEdge(
|
86 |
-
targetNode: ComfyUIWorkflowApiGraphNode,
|
87 |
-
relationship: string,
|
88 |
-
metadata?: any
|
89 |
-
) {
|
90 |
-
const edge = new ComfyUIWorkflowApiGraphEdge(
|
91 |
-
this,
|
92 |
-
targetNode,
|
93 |
-
relationship,
|
94 |
-
metadata
|
95 |
-
)
|
96 |
-
this.outboundEdges.push(edge)
|
97 |
-
targetNode.inboundEdges.push(edge)
|
98 |
-
}
|
99 |
-
|
100 |
-
addInboundEdge(
|
101 |
-
sourceNode: ComfyUIWorkflowApiGraphNode,
|
102 |
-
relationship: string,
|
103 |
-
metadata?: any
|
104 |
-
) {
|
105 |
-
const edge = new ComfyUIWorkflowApiGraphEdge(
|
106 |
-
sourceNode,
|
107 |
-
this,
|
108 |
-
relationship,
|
109 |
-
metadata
|
110 |
-
)
|
111 |
-
this.inboundEdges.push(edge)
|
112 |
-
sourceNode.outboundEdges.push(edge)
|
113 |
-
}
|
114 |
-
|
115 |
-
getOutboundEdges(): ComfyUIWorkflowApiGraphEdge[] {
|
116 |
-
return this.outboundEdges
|
117 |
-
}
|
118 |
-
|
119 |
-
getInboundEdges(): ComfyUIWorkflowApiGraphEdge[] {
|
120 |
-
return this.inboundEdges
|
121 |
-
}
|
122 |
-
|
123 |
-
toJson(): Record<string, any> {
|
124 |
-
return structuredClone({
|
125 |
-
inputs: this.inputs,
|
126 |
-
class_type: this.class_type,
|
127 |
-
_meta: this._meta,
|
128 |
-
})
|
129 |
-
}
|
130 |
-
}
|
131 |
-
|
132 |
-
/**
|
133 |
-
* Utils to query ComfyUI workflow-api nodes data
|
134 |
-
*/
|
135 |
-
export class ComfyUIWorkflowApiGraph {
|
136 |
-
private json: ComfyUIWorkflowApiJson
|
137 |
-
private nodes: Record<string, ComfyUIWorkflowApiGraphNode> = {}
|
138 |
-
private adjList: Record<string, string[]> = {}
|
139 |
-
private dependencyList: Record<
|
140 |
-
string,
|
141 |
-
{ from: string; inputName: string }[]
|
142 |
-
> = {}
|
143 |
-
private dependantList: Record<string, { to: string; inputName: string }[]> =
|
144 |
-
{}
|
145 |
-
private inDegree: Record<string, number> = {}
|
146 |
-
|
147 |
-
constructor(workflow: ComfyUIWorkflowApiJson) {
|
148 |
-
this.json = structuredClone(workflow)
|
149 |
-
const { adjList, dependencyList, dependantList, inDegree } =
|
150 |
-
this.buildGraphData()
|
151 |
-
this.adjList = adjList
|
152 |
-
this.dependencyList = dependencyList
|
153 |
-
this.dependantList = dependantList
|
154 |
-
this.inDegree = inDegree
|
155 |
-
const hasCycles = this.detectCycles()
|
156 |
-
if (hasCycles) {
|
157 |
-
throw new Error(
|
158 |
-
'The provided workflow has cycles, impossible to get the output node.'
|
159 |
-
)
|
160 |
-
}
|
161 |
-
}
|
162 |
-
|
163 |
-
/**
|
164 |
-
* Create a graph structure in an adjacent list
|
165 |
-
* representation with additional data arrays
|
166 |
-
* for dev purposes.
|
167 |
-
* @param workflow
|
168 |
-
* @returns
|
169 |
-
*/
|
170 |
-
private buildGraphData() {
|
171 |
-
const adjList: Record<string, string[]> = {}
|
172 |
-
const dependencyList: Record<
|
173 |
-
string,
|
174 |
-
{ from: string; inputName: string }[]
|
175 |
-
> = {}
|
176 |
-
const dependantList: Record<string, { to: string; inputName: string }[]> =
|
177 |
-
{}
|
178 |
-
const inDegree: Record<string, number> = {}
|
179 |
-
|
180 |
-
for (const nodeId of Object.keys(this.json)) {
|
181 |
-
const node = new ComfyUIWorkflowApiGraphNode(nodeId)
|
182 |
-
node.inputs = this.json[nodeId].inputs
|
183 |
-
node._meta = this.json[nodeId]._meta
|
184 |
-
node.class_type = this.json[nodeId].class_type
|
185 |
-
this.nodes[nodeId] = node
|
186 |
-
|
187 |
-
adjList[nodeId] = []
|
188 |
-
dependencyList[nodeId] = []
|
189 |
-
dependantList[nodeId] = []
|
190 |
-
inDegree[nodeId] = 0
|
191 |
-
}
|
192 |
-
|
193 |
-
for (const node of Object.values(this.nodes)) {
|
194 |
-
if (node.inputs) {
|
195 |
-
for (const [inputName, value] of Object.entries(node.inputs)) {
|
196 |
-
if (Array.isArray(value)) {
|
197 |
-
const dependencyNodeId = value[0] as string
|
198 |
-
adjList[dependencyNodeId].push(node.id)
|
199 |
-
dependencyList[node.id].push({ from: dependencyNodeId, inputName })
|
200 |
-
node.addInboundEdge(this.nodes[dependencyNodeId], inputName)
|
201 |
-
dependantList[dependencyNodeId].push({ to: node.id, inputName })
|
202 |
-
inDegree[node.id] += 1
|
203 |
-
}
|
204 |
-
}
|
205 |
-
}
|
206 |
-
}
|
207 |
-
|
208 |
-
return { adjList, dependencyList, dependantList, inDegree }
|
209 |
-
}
|
210 |
-
|
211 |
-
/**
|
212 |
-
* Check for corrupted workflows with loops
|
213 |
-
*/
|
214 |
-
private detectCycles(): boolean {
|
215 |
-
const visited: Record<string, boolean> = {}
|
216 |
-
const recursionStack: Record<string, boolean> = {}
|
217 |
-
const dfs = (nodeId: string): boolean => {
|
218 |
-
if (!visited[nodeId]) {
|
219 |
-
visited[nodeId] = true
|
220 |
-
recursionStack[nodeId] = true
|
221 |
-
for (const neighbor of this.adjList[nodeId]) {
|
222 |
-
if (!visited[neighbor] && dfs(neighbor)) {
|
223 |
-
return true
|
224 |
-
} else if (recursionStack[neighbor]) {
|
225 |
-
return true
|
226 |
-
}
|
227 |
-
}
|
228 |
-
}
|
229 |
-
|
230 |
-
recursionStack[nodeId] = false
|
231 |
-
return false
|
232 |
-
}
|
233 |
-
|
234 |
-
for (const nodeId of Object.keys(this.adjList)) {
|
235 |
-
if (dfs(nodeId)) {
|
236 |
-
return true
|
237 |
-
}
|
238 |
-
}
|
239 |
-
|
240 |
-
return false
|
241 |
-
}
|
242 |
-
|
243 |
-
getGraphData() {
|
244 |
-
const { adjList, dependencyList, dependantList, inDegree } = this
|
245 |
-
return { adjList, dependencyList, dependantList, inDegree }
|
246 |
-
}
|
247 |
-
|
248 |
-
/**
|
249 |
-
* Get all nodes that have inputs.
|
250 |
-
*/
|
251 |
-
getNodesWithInputs(): ComfyUIWorkflowApiGraphNode[] {
|
252 |
-
const nodesWithInputs: ComfyUIWorkflowApiGraphNode[] = []
|
253 |
-
for (const node of Object.values(this.nodes)) {
|
254 |
-
if (node.inputs && Object.keys(node.inputs).length > 0) {
|
255 |
-
nodesWithInputs.push(node)
|
256 |
-
}
|
257 |
-
}
|
258 |
-
return nodesWithInputs
|
259 |
-
}
|
260 |
-
|
261 |
-
getNodes(): ComfyUIWorkflowApiGraphNode[] {
|
262 |
-
return Object.values(structuredClone(this.nodes))
|
263 |
-
}
|
264 |
-
|
265 |
-
getNodesDict(): Record<string, ComfyUIWorkflowApiGraphNode> {
|
266 |
-
return structuredClone(this.nodes)
|
267 |
-
}
|
268 |
-
|
269 |
-
/**
|
270 |
-
* Returns a copy of all inputs in the workflow.
|
271 |
-
*/
|
272 |
-
getInputs(): Record<string, ComfyUiWorkflowApiNodeInput> {
|
273 |
-
const nodesWithInputs = this.getNodesWithInputs()
|
274 |
-
const inputs = {}
|
275 |
-
|
276 |
-
for (const node of nodesWithInputs) {
|
277 |
-
const inputSchemas = this.getInputsByNodeId(node.id)
|
278 |
-
inputSchemas?.forEach((inputSchema) => {
|
279 |
-
inputs[`${inputSchema.node.id}.inputs.${inputSchema.name}`] =
|
280 |
-
inputSchema
|
281 |
-
})
|
282 |
-
}
|
283 |
-
|
284 |
-
return inputs
|
285 |
-
}
|
286 |
-
|
287 |
-
/**
|
288 |
-
* Topological sort of the graph (Kahn's Algorithm) to get the final output node.
|
289 |
-
* TODO: multiple outputs.
|
290 |
-
*/
|
291 |
-
getOutputNode(): NodeData | null {
|
292 |
-
const { adjList, inDegree } = this
|
293 |
-
const queue: string[] = []
|
294 |
-
const sortedOrder: string[] = []
|
295 |
-
|
296 |
-
for (const nodeId of Object.keys(inDegree)) {
|
297 |
-
if (inDegree[nodeId] === 0) {
|
298 |
-
queue.push(nodeId)
|
299 |
-
}
|
300 |
-
}
|
301 |
-
|
302 |
-
while (queue.length > 0) {
|
303 |
-
const currentNode = queue.shift()!
|
304 |
-
sortedOrder.push(currentNode)
|
305 |
-
for (const neighbor of adjList[currentNode]) {
|
306 |
-
inDegree[neighbor] -= 1
|
307 |
-
if (inDegree[neighbor] === 0) {
|
308 |
-
queue.push(neighbor)
|
309 |
-
}
|
310 |
-
}
|
311 |
-
}
|
312 |
-
|
313 |
-
// Last node in sortedOrder is the output node
|
314 |
-
// TODO: handle multiple outputs
|
315 |
-
if (sortedOrder.length === Object.keys(this.json).length) {
|
316 |
-
const outputNodeId = sortedOrder[sortedOrder.length - 1]
|
317 |
-
return { id: outputNodeId, ...this.json[outputNodeId] }
|
318 |
-
} else {
|
319 |
-
// If there are cycles, fail
|
320 |
-
throw new Error(
|
321 |
-
'The provided workflow has cycles, impossible to get the output node.'
|
322 |
-
)
|
323 |
-
}
|
324 |
-
}
|
325 |
-
|
326 |
-
/**
|
327 |
-
* Get all value inputs of a given node in the workflow.
|
328 |
-
* Ignore input connections (e.g. inputs with value ['3', 0])
|
329 |
-
* @param nodeId the id of the node
|
330 |
-
*/
|
331 |
-
getInputsByNodeId(nodeId: string): ComfyUiWorkflowApiNodeInput[] | null {
|
332 |
-
const nodeData = this.nodes[nodeId]
|
333 |
-
if (!nodeData || !nodeData.inputs) {
|
334 |
-
return null
|
335 |
-
}
|
336 |
-
|
337 |
-
const inputs: ComfyUiWorkflowApiNodeInput[] = []
|
338 |
-
|
339 |
-
for (const [name, value] of Object.entries(nodeData.inputs)) {
|
340 |
-
if (Array.isArray(value)) continue
|
341 |
-
|
342 |
-
// TODO: Handle more types
|
343 |
-
let inputType: INPUT_TYPES =
|
344 |
-
typeof value === 'string' ? 'string' : 'number'
|
345 |
-
|
346 |
-
inputs.push({
|
347 |
-
type: inputType,
|
348 |
-
name: name,
|
349 |
-
value: value,
|
350 |
-
id: `${nodeId}.inputs.${name}`,
|
351 |
-
node: {
|
352 |
-
id: nodeId,
|
353 |
-
type: nodeData.class_type,
|
354 |
-
name: nodeData?._meta?.title,
|
355 |
-
},
|
356 |
-
})
|
357 |
-
}
|
358 |
-
|
359 |
-
return inputs.length > 0 ? inputs : null
|
360 |
-
}
|
361 |
-
|
362 |
-
/**
|
363 |
-
* A simple search of workflow inputs
|
364 |
-
* @param query
|
365 |
-
*/
|
366 |
-
findInput(query: {
|
367 |
-
// By type of node input
|
368 |
-
type?: string | RegExp
|
369 |
-
// By name of node input
|
370 |
-
name?: string | RegExp
|
371 |
-
// Based on the node
|
372 |
-
nodeType?: string | RegExp
|
373 |
-
nodeName?: string | RegExp
|
374 |
-
// If any output of the node containing the input
|
375 |
-
// is targeting another node's input with the given
|
376 |
-
// name
|
377 |
-
nodeOutputToNodeInput?: string | RegExp
|
378 |
-
// By value
|
379 |
-
value?: (value) => boolean
|
380 |
-
}): ComfyUiWorkflowApiNodeInput[] {
|
381 |
-
const inputs = this.getInputs()
|
382 |
-
|
383 |
-
// Helper function to match string or RegExp
|
384 |
-
const matches = (
|
385 |
-
value: string | undefined,
|
386 |
-
query: string | RegExp | undefined
|
387 |
-
): boolean => {
|
388 |
-
if (!query) return true
|
389 |
-
if (typeof query === 'string') {
|
390 |
-
return value === query
|
391 |
-
} else if (query instanceof RegExp) {
|
392 |
-
return query.test(value || '')
|
393 |
-
}
|
394 |
-
return false
|
395 |
-
}
|
396 |
-
|
397 |
-
return Object.values(inputs).filter(
|
398 |
-
(input) =>
|
399 |
-
(matches(input.type, query.type) &&
|
400 |
-
matches(input.name, query.name) &&
|
401 |
-
matches(input.node.name, query.nodeName) &&
|
402 |
-
matches(input.node.type, query.nodeType) &&
|
403 |
-
(!query.nodeOutputToNodeInput ||
|
404 |
-
this.nodes[input.node.id].outboundEdges.some((edge) =>
|
405 |
-
matches(edge.relationship, query.nodeOutputToNodeInput)
|
406 |
-
)) &&
|
407 |
-
!query.value) ||
|
408 |
-
query.value?.(input.value)
|
409 |
-
)
|
410 |
-
}
|
411 |
-
|
412 |
-
setInputValue(
|
413 |
-
inputKey: string,
|
414 |
-
value: any,
|
415 |
-
options?: {
|
416 |
-
ignoreErrors?: boolean
|
417 |
-
}
|
418 |
-
) {
|
419 |
-
const inputs = this.getInputs()
|
420 |
-
if (!options?.ignoreErrors && !inputs[inputKey]) {
|
421 |
-
throw new Error("Input doesn't exist in the workflow")
|
422 |
-
}
|
423 |
-
const input = inputs[inputKey]
|
424 |
-
if (!input) return
|
425 |
-
if (!this.nodes[input.node.id].inputs) this.nodes[input.node.id].inputs = {}
|
426 |
-
this.nodes[input.node.id].inputs![input.name] = value
|
427 |
-
}
|
428 |
-
|
429 |
-
toJson(): Record<string, any> {
|
430 |
-
const nodes = this.nodes
|
431 |
-
const nodesJson = {}
|
432 |
-
for (const key of Object.keys(nodes)) {
|
433 |
-
nodesJson[key] = nodes[key].toJson()
|
434 |
-
}
|
435 |
-
return nodesJson
|
436 |
-
}
|
437 |
-
|
438 |
-
toString() {
|
439 |
-
return JSON.stringify(this.toJson())
|
440 |
-
}
|
441 |
-
|
442 |
-
static fromString(
|
443 |
-
workflowString: string | undefined
|
444 |
-
): ComfyUIWorkflowApiGraph {
|
445 |
-
if (!workflowString) throw new Error('Invalid workflow.')
|
446 |
-
const workflowRaw = JSON.parse(workflowString)
|
447 |
-
return new ComfyUIWorkflowApiGraph(workflowRaw)
|
448 |
-
}
|
449 |
-
|
450 |
-
static isValidWorkflow(workflowString: string | undefined): boolean {
|
451 |
-
try {
|
452 |
-
ComfyUIWorkflowApiGraph.fromString(workflowString)
|
453 |
-
return true
|
454 |
-
} catch {}
|
455 |
-
return false
|
456 |
-
}
|
457 |
-
}
|
458 |
|
459 |
/**
|
460 |
* Shortcut methods to query Clapper related data from a workflow
|
@@ -565,32 +112,6 @@ export function findImageInputsFromWorkflow(
|
|
565 |
)
|
566 |
}
|
567 |
|
568 |
-
export const getMainInputIdsByClapWorkflowCategory = (
|
569 |
-
category: ClapWorkflowCategory
|
570 |
-
) => {
|
571 |
-
switch (category) {
|
572 |
-
case ClapWorkflowCategory.VIDEO_GENERATION: {
|
573 |
-
return [
|
574 |
-
ClapperComfyUiInputIds.IMAGE,
|
575 |
-
ClapperComfyUiInputIds.WIDTH,
|
576 |
-
ClapperComfyUiInputIds.HEIGHT,
|
577 |
-
ClapperComfyUiInputIds.SEED,
|
578 |
-
ClapperComfyUiInputIds.OUTPUT,
|
579 |
-
]
|
580 |
-
}
|
581 |
-
default: {
|
582 |
-
return [
|
583 |
-
ClapperComfyUiInputIds.PROMPT,
|
584 |
-
ClapperComfyUiInputIds.NEGATIVE_PROMPT,
|
585 |
-
ClapperComfyUiInputIds.WIDTH,
|
586 |
-
ClapperComfyUiInputIds.HEIGHT,
|
587 |
-
ClapperComfyUiInputIds.SEED,
|
588 |
-
ClapperComfyUiInputIds.OUTPUT,
|
589 |
-
]
|
590 |
-
}
|
591 |
-
}
|
592 |
-
}
|
593 |
-
|
594 |
export const MainClapWorkflowInputsLabels = {
|
595 |
[ClapperComfyUiInputIds.PROMPT]: 'Prompt node input',
|
596 |
[ClapperComfyUiInputIds.NEGATIVE_PROMPT]: 'Negative prompt node input',
|
@@ -600,427 +121,3 @@ export const MainClapWorkflowInputsLabels = {
|
|
600 |
[ClapperComfyUiInputIds.IMAGE]: 'Image node input',
|
601 |
[ClapperComfyUiInputIds.OUTPUT]: 'Output node',
|
602 |
}
|
603 |
-
|
604 |
-
export function getMainInputsFromComfyUiWorkflow(
|
605 |
-
workflowString: string,
|
606 |
-
category: ClapWorkflowCategory
|
607 |
-
): {
|
608 |
-
inputFields: ClapInputFields
|
609 |
-
inputValues: ClapInputValues
|
610 |
-
} {
|
611 |
-
const workflowGraph = ComfyUIWorkflowApiGraph.fromString(workflowString)
|
612 |
-
const mainInputsIds = getMainInputIdsByClapWorkflowCategory(category)
|
613 |
-
const nodes = workflowGraph.getNodes()
|
614 |
-
const textInputs = workflowGraph.findInput({
|
615 |
-
type: 'string',
|
616 |
-
name: /.*(text|prompt).*/,
|
617 |
-
})
|
618 |
-
const dimensionInputs = workflowGraph.findInput({
|
619 |
-
type: 'number',
|
620 |
-
name: /.*(width|height).*/,
|
621 |
-
})
|
622 |
-
const promptNodeInputs = findPromptInputsFromWorkflow(workflowGraph)
|
623 |
-
const negativePromptNodeInputs =
|
624 |
-
findNegativePromptInputsFromWorkflow(workflowGraph)
|
625 |
-
const widthNodeInputs = findWidthInputsFromWorkflow(workflowGraph)
|
626 |
-
const heightNodeInputs = findHeightInputsFromWorkflow(workflowGraph)
|
627 |
-
const seedNodeInputs = findSeedInputsFromWorkflow(workflowGraph)
|
628 |
-
const imageNodeInputs = findImageInputsFromWorkflow(workflowGraph)
|
629 |
-
const outputNode = workflowGraph.getOutputNode()
|
630 |
-
|
631 |
-
const inputValues = {
|
632 |
-
[ClapperComfyUiInputIds.PROMPT]: promptNodeInputs?.[0]
|
633 |
-
? {
|
634 |
-
id: promptNodeInputs?.[0].id,
|
635 |
-
label: `${promptNodeInputs?.[0].id} (from node ${promptNodeInputs?.[0].node.id})`,
|
636 |
-
}
|
637 |
-
: undefined,
|
638 |
-
[ClapperComfyUiInputIds.NEGATIVE_PROMPT]: negativePromptNodeInputs?.[0]
|
639 |
-
? {
|
640 |
-
id: negativePromptNodeInputs?.[0].id,
|
641 |
-
label: `${negativePromptNodeInputs?.[0].id} (from node ${negativePromptNodeInputs?.[0].node.id})`,
|
642 |
-
}
|
643 |
-
: undefined,
|
644 |
-
[ClapperComfyUiInputIds.WIDTH]: widthNodeInputs?.[0]
|
645 |
-
? {
|
646 |
-
id: widthNodeInputs?.[0].id,
|
647 |
-
label: `${widthNodeInputs?.[0].id} (from node ${widthNodeInputs?.[0].node.id})`,
|
648 |
-
}
|
649 |
-
: undefined,
|
650 |
-
[ClapperComfyUiInputIds.HEIGHT]: heightNodeInputs?.[0]
|
651 |
-
? {
|
652 |
-
id: heightNodeInputs?.[0].id,
|
653 |
-
label: `${heightNodeInputs?.[0].id} (from node ${heightNodeInputs?.[0].node.id})`,
|
654 |
-
}
|
655 |
-
: undefined,
|
656 |
-
[ClapperComfyUiInputIds.SEED]: seedNodeInputs?.[0]
|
657 |
-
? {
|
658 |
-
id: seedNodeInputs?.[0].id,
|
659 |
-
label: `${seedNodeInputs?.[0].id} (from node ${seedNodeInputs?.[0].node.id})`,
|
660 |
-
}
|
661 |
-
: undefined,
|
662 |
-
[ClapperComfyUiInputIds.IMAGE]: imageNodeInputs?.[0]
|
663 |
-
? {
|
664 |
-
id: imageNodeInputs?.[0].id,
|
665 |
-
label: `${imageNodeInputs?.[0].id} (from node ${imageNodeInputs?.[0].node.id})`,
|
666 |
-
}
|
667 |
-
: undefined,
|
668 |
-
[ClapperComfyUiInputIds.OUTPUT]: outputNode
|
669 |
-
? {
|
670 |
-
id: outputNode?.id,
|
671 |
-
label: `${outputNode?._meta?.title} (id: ${outputNode?.id})`,
|
672 |
-
}
|
673 |
-
: undefined,
|
674 |
-
}
|
675 |
-
|
676 |
-
const getOptionsItems = (inputs: ComfyUiWorkflowApiNodeInput[]) => {
|
677 |
-
return [
|
678 |
-
...inputs,
|
679 |
-
{
|
680 |
-
id: ClapperComfyUiInputIds.NULL,
|
681 |
-
name: 'Unset',
|
682 |
-
node: {
|
683 |
-
id: null,
|
684 |
-
},
|
685 |
-
},
|
686 |
-
].map((p) => {
|
687 |
-
const item = {
|
688 |
-
id: p.id,
|
689 |
-
label:
|
690 |
-
p.id === ClapperComfyUiInputIds.NULL
|
691 |
-
? `Unset`
|
692 |
-
: `${p.name} (from node ${p.node.id})`,
|
693 |
-
}
|
694 |
-
return {
|
695 |
-
...item,
|
696 |
-
value: item,
|
697 |
-
}
|
698 |
-
})
|
699 |
-
}
|
700 |
-
|
701 |
-
const inputFields: any = []
|
702 |
-
mainInputsIds.forEach((mainInput) => {
|
703 |
-
switch (mainInput) {
|
704 |
-
case ClapperComfyUiInputIds.PROMPT: {
|
705 |
-
inputFields.push({
|
706 |
-
id: ClapperComfyUiInputIds.PROMPT,
|
707 |
-
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.PROMPT],
|
708 |
-
type: 'nodeInput' as any,
|
709 |
-
category: ClapInputCategory.PROMPT,
|
710 |
-
description: 'The input where Clapper will put the segment prompt',
|
711 |
-
defaultValue: '',
|
712 |
-
metadata: {
|
713 |
-
options: getOptionsItems(
|
714 |
-
unionBy(promptNodeInputs, textInputs, 'id')
|
715 |
-
),
|
716 |
-
},
|
717 |
-
})
|
718 |
-
return
|
719 |
-
}
|
720 |
-
case ClapperComfyUiInputIds.NEGATIVE_PROMPT: {
|
721 |
-
inputFields.push({
|
722 |
-
id: ClapperComfyUiInputIds.NEGATIVE_PROMPT,
|
723 |
-
label:
|
724 |
-
MainClapWorkflowInputsLabels[
|
725 |
-
ClapperComfyUiInputIds.NEGATIVE_PROMPT
|
726 |
-
],
|
727 |
-
type: 'nodeInput' as any,
|
728 |
-
category: ClapInputCategory.PROMPT,
|
729 |
-
description:
|
730 |
-
'The node input where Clapper will put the segment negative prompt',
|
731 |
-
defaultValue: '',
|
732 |
-
metadata: {
|
733 |
-
options: getOptionsItems(
|
734 |
-
unionBy(negativePromptNodeInputs, textInputs, 'id')
|
735 |
-
),
|
736 |
-
},
|
737 |
-
})
|
738 |
-
return
|
739 |
-
}
|
740 |
-
case ClapperComfyUiInputIds.WIDTH: {
|
741 |
-
inputFields.push({
|
742 |
-
id: ClapperComfyUiInputIds.WIDTH,
|
743 |
-
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.WIDTH],
|
744 |
-
type: 'nodeInput' as any,
|
745 |
-
category: ClapInputCategory.WIDTH,
|
746 |
-
description:
|
747 |
-
'The node input where Clapper will put the required image width',
|
748 |
-
defaultValue: '',
|
749 |
-
metadata: {
|
750 |
-
options: getOptionsItems(
|
751 |
-
unionBy(widthNodeInputs, dimensionInputs, 'id')
|
752 |
-
),
|
753 |
-
},
|
754 |
-
})
|
755 |
-
return
|
756 |
-
}
|
757 |
-
case ClapperComfyUiInputIds.HEIGHT: {
|
758 |
-
inputFields.push({
|
759 |
-
id: ClapperComfyUiInputIds.HEIGHT,
|
760 |
-
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.HEIGHT],
|
761 |
-
type: 'nodeInput' as any,
|
762 |
-
description:
|
763 |
-
'The node input where Clapper will put the required image height',
|
764 |
-
category: ClapInputCategory.HEIGHT,
|
765 |
-
defaultValue: 1000,
|
766 |
-
metadata: {
|
767 |
-
options: getOptionsItems(
|
768 |
-
unionBy(heightNodeInputs, dimensionInputs, 'id')
|
769 |
-
),
|
770 |
-
},
|
771 |
-
})
|
772 |
-
return
|
773 |
-
}
|
774 |
-
case ClapperComfyUiInputIds.SEED: {
|
775 |
-
inputFields.push({
|
776 |
-
id: ClapperComfyUiInputIds.SEED,
|
777 |
-
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.SEED],
|
778 |
-
type: 'nodeInput' as any,
|
779 |
-
category: ClapInputCategory.UNKNOWN,
|
780 |
-
description: 'The node input where Clapper will set a seed',
|
781 |
-
defaultValue: '',
|
782 |
-
metadata: {
|
783 |
-
options: getOptionsItems(seedNodeInputs),
|
784 |
-
},
|
785 |
-
})
|
786 |
-
return
|
787 |
-
}
|
788 |
-
case ClapperComfyUiInputIds.IMAGE: {
|
789 |
-
inputFields.push({
|
790 |
-
id: ClapperComfyUiInputIds.IMAGE,
|
791 |
-
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.IMAGE],
|
792 |
-
type: 'nodeInput' as any,
|
793 |
-
category: ClapInputCategory.UNKNOWN,
|
794 |
-
description: 'The node input where Clapper will set an image',
|
795 |
-
defaultValue: '',
|
796 |
-
metadata: {
|
797 |
-
options: getOptionsItems(imageNodeInputs),
|
798 |
-
tooltip: {
|
799 |
-
message: `
|
800 |
-
Clapper doesn't support file/upload node inputs;
|
801 |
-
use a string input from where Clapper can load a base64
|
802 |
-
data URI string (e.g. the "Load Image From Base64" node's
|
803 |
-
"data" input in the default video workflow).
|
804 |
-
`,
|
805 |
-
type: 'info',
|
806 |
-
},
|
807 |
-
},
|
808 |
-
})
|
809 |
-
return
|
810 |
-
}
|
811 |
-
case ClapperComfyUiInputIds.OUTPUT: {
|
812 |
-
inputFields.push({
|
813 |
-
id: ClapperComfyUiInputIds.OUTPUT,
|
814 |
-
label: MainClapWorkflowInputsLabels[ClapperComfyUiInputIds.OUTPUT],
|
815 |
-
type: 'node' as any,
|
816 |
-
category: ClapInputCategory.UNKNOWN,
|
817 |
-
description: 'The node from which Clapper will get the output image',
|
818 |
-
defaultValue: '',
|
819 |
-
metadata: {
|
820 |
-
options: nodes.map((p) => {
|
821 |
-
const item = {
|
822 |
-
id: p.id,
|
823 |
-
label: `${p._meta?.title || 'Untitled node'} (id: ${p.id})`,
|
824 |
-
}
|
825 |
-
return {
|
826 |
-
...item,
|
827 |
-
value: item,
|
828 |
-
}
|
829 |
-
}),
|
830 |
-
},
|
831 |
-
})
|
832 |
-
return
|
833 |
-
}
|
834 |
-
}
|
835 |
-
})
|
836 |
-
|
837 |
-
return {
|
838 |
-
inputFields,
|
839 |
-
inputValues,
|
840 |
-
}
|
841 |
-
}
|
842 |
-
|
843 |
-
/**
|
844 |
-
* Returns input fields / input values required by ComfyUi
|
845 |
-
* @param workflow
|
846 |
-
*/
|
847 |
-
export function getInputsFromComfyUiWorkflow(
|
848 |
-
workflowString: string,
|
849 |
-
category: ClapWorkflowCategory
|
850 |
-
): {
|
851 |
-
inputFields: ClapInputFields
|
852 |
-
inputValues: ClapInputValues
|
853 |
-
} {
|
854 |
-
const workflowGraph = ComfyUIWorkflowApiGraph.fromString(workflowString)
|
855 |
-
const { inputFields: mainInputFields, inputValues: mainInputValues } =
|
856 |
-
getMainInputsFromComfyUiWorkflow(workflowString, category)
|
857 |
-
|
858 |
-
const inputValues = {
|
859 |
-
...mainInputValues,
|
860 |
-
} as any
|
861 |
-
|
862 |
-
const inputFields: ClapInputField<{
|
863 |
-
options?: {
|
864 |
-
id: string
|
865 |
-
label: string
|
866 |
-
value: any
|
867 |
-
}[]
|
868 |
-
tooltip?: {
|
869 |
-
type: string
|
870 |
-
message: string
|
871 |
-
}
|
872 |
-
}>[] = [
|
873 |
-
// Required fields that should be available in the workflow, otherwise
|
874 |
-
// Clapper doesn't know how to input its settings (prompts, dimensions, etc)
|
875 |
-
{
|
876 |
-
id: '@clapper/mainInputs',
|
877 |
-
label: 'Main settings',
|
878 |
-
type: 'group' as any,
|
879 |
-
category: ClapInputCategory.UNKNOWN,
|
880 |
-
description: 'Main inputs',
|
881 |
-
defaultValue: '',
|
882 |
-
inputFields: mainInputFields,
|
883 |
-
},
|
884 |
-
// Other input fields based on the workflow nodes
|
885 |
-
{
|
886 |
-
id: '@clapper/otherInputs',
|
887 |
-
label: 'Node settings',
|
888 |
-
type: 'group' as any,
|
889 |
-
category: ClapInputCategory.UNKNOWN,
|
890 |
-
description: 'Main inputs',
|
891 |
-
defaultValue: '',
|
892 |
-
inputFields: workflowGraph
|
893 |
-
.getNodesWithInputs()
|
894 |
-
// Discard nodes with only inputs connections
|
895 |
-
.filter(({ id }) => workflowGraph.getInputsByNodeId(id)?.length)
|
896 |
-
.map(({ id, _meta }) => {
|
897 |
-
return {
|
898 |
-
id: '@clapper/node/' + id,
|
899 |
-
label: `${_meta?.title} (id: ${id})`,
|
900 |
-
type: 'group' as any,
|
901 |
-
category: ClapInputCategory.UNKNOWN,
|
902 |
-
description: `Settings for ${_meta?.title}`,
|
903 |
-
defaultValue: '',
|
904 |
-
inputFields: workflowGraph.getInputsByNodeId(id)?.map((input) => {
|
905 |
-
const mainInputKey = Object.keys(inputValues).find(
|
906 |
-
(key) => inputValues[key]?.id == input.id
|
907 |
-
)
|
908 |
-
inputValues[input.id] = input.value
|
909 |
-
return {
|
910 |
-
id: input.id,
|
911 |
-
label: input.name,
|
912 |
-
type: input.type as any,
|
913 |
-
category: ClapInputCategory.UNKNOWN,
|
914 |
-
description: '',
|
915 |
-
defaultValue: input.value,
|
916 |
-
metadata: {
|
917 |
-
tooltip: MainClapWorkflowInputsLabels[mainInputKey as string]
|
918 |
-
? {
|
919 |
-
type: 'warning',
|
920 |
-
message: `This value will be overwritten by Clapper because it is
|
921 |
-
used as "${MainClapWorkflowInputsLabels[mainInputKey as string]}".`,
|
922 |
-
}
|
923 |
-
: undefined,
|
924 |
-
},
|
925 |
-
}
|
926 |
-
}),
|
927 |
-
}
|
928 |
-
}),
|
929 |
-
},
|
930 |
-
]
|
931 |
-
|
932 |
-
return {
|
933 |
-
inputFields,
|
934 |
-
inputValues,
|
935 |
-
}
|
936 |
-
}
|
937 |
-
|
938 |
-
/**
|
939 |
-
* Takes a workflow graph and converts it to PromptBuilder
|
940 |
-
*/
|
941 |
-
export function createPromptBuilder(
|
942 |
-
workflowApiGraph: ComfyUIWorkflowApiGraph
|
943 |
-
): PromptBuilder<any, any, any> {
|
944 |
-
const inputs = workflowApiGraph.getInputs()
|
945 |
-
const outputNode = workflowApiGraph.getOutputNode()
|
946 |
-
|
947 |
-
const inputKeys = Object.values(inputs)
|
948 |
-
.map((input) => input.id)
|
949 |
-
.concat([
|
950 |
-
ClapperComfyUiInputIds.PROMPT,
|
951 |
-
ClapperComfyUiInputIds.NEGATIVE_PROMPT,
|
952 |
-
ClapperComfyUiInputIds.WIDTH,
|
953 |
-
ClapperComfyUiInputIds.HEIGHT,
|
954 |
-
ClapperComfyUiInputIds.SEED,
|
955 |
-
])
|
956 |
-
|
957 |
-
const promptBuilder = new PromptBuilder(
|
958 |
-
workflowApiGraph.toJson(),
|
959 |
-
inputKeys,
|
960 |
-
[ClapperComfyUiInputIds.OUTPUT]
|
961 |
-
)
|
962 |
-
|
963 |
-
// We don't need proper names for input keys,
|
964 |
-
// as we just use PromptBuilder for its websocket api
|
965 |
-
inputKeys.forEach((inputKey) => {
|
966 |
-
promptBuilder.setInputNode(inputKey, inputKey)
|
967 |
-
})
|
968 |
-
|
969 |
-
if (outputNode) {
|
970 |
-
promptBuilder.setOutputNode(ClapperComfyUiInputIds.OUTPUT, outputNode.id)
|
971 |
-
}
|
972 |
-
|
973 |
-
return promptBuilder
|
974 |
-
}
|
975 |
-
|
976 |
-
export function convertComfyUiWorkflowApiToClapWorkflow(
|
977 |
-
workflowString: string,
|
978 |
-
category: ClapWorkflowCategory = ClapWorkflowCategory.IMAGE_GENERATION
|
979 |
-
): ClapWorkflow {
|
980 |
-
try {
|
981 |
-
const { inputFields, inputValues } = getInputsFromComfyUiWorkflow(
|
982 |
-
workflowString,
|
983 |
-
category
|
984 |
-
)
|
985 |
-
switch (category) {
|
986 |
-
case ClapWorkflowCategory.VIDEO_GENERATION: {
|
987 |
-
return {
|
988 |
-
id: 'comfyui://settings.comfyWorkflowForVideo',
|
989 |
-
label: 'Custom Video Workflow',
|
990 |
-
description: 'Custom ComfyUI workflow to generate videos',
|
991 |
-
tags: ['custom', 'video generation'],
|
992 |
-
author: 'You',
|
993 |
-
thumbnailUrl: '',
|
994 |
-
nonCommercial: false,
|
995 |
-
engine: ClapWorkflowEngine.COMFYUI_WORKFLOW,
|
996 |
-
provider: ClapWorkflowProvider.COMFYUI,
|
997 |
-
category,
|
998 |
-
data: workflowString,
|
999 |
-
schema: '',
|
1000 |
-
inputFields,
|
1001 |
-
inputValues,
|
1002 |
-
}
|
1003 |
-
}
|
1004 |
-
default: {
|
1005 |
-
return {
|
1006 |
-
id: 'comfyui://settings.comfyWorkflowForImage',
|
1007 |
-
label: 'Custom Image Workflow',
|
1008 |
-
description: 'Custom ComfyUI workflow to generate images',
|
1009 |
-
tags: ['custom', 'image generation'],
|
1010 |
-
author: 'You',
|
1011 |
-
thumbnailUrl: '',
|
1012 |
-
nonCommercial: false,
|
1013 |
-
engine: ClapWorkflowEngine.COMFYUI_WORKFLOW,
|
1014 |
-
provider: ClapWorkflowProvider.COMFYUI,
|
1015 |
-
category,
|
1016 |
-
data: workflowString,
|
1017 |
-
schema: '',
|
1018 |
-
inputFields,
|
1019 |
-
inputValues,
|
1020 |
-
}
|
1021 |
-
}
|
1022 |
-
}
|
1023 |
-
} catch (e) {
|
1024 |
-
throw e
|
1025 |
-
}
|
1026 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import unionBy from 'lodash/unionBy'
|
2 |
|
3 |
+
import { ClapperComfyUiInputIds, ComfyUiWorkflowApiNodeInput } from './types'
|
4 |
+
import { ComfyUIWorkflowApiGraph } from './graph'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
/**
|
7 |
* Shortcut methods to query Clapper related data from a workflow
|
|
|
112 |
)
|
113 |
}
|
114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
export const MainClapWorkflowInputsLabels = {
|
116 |
[ClapperComfyUiInputIds.PROMPT]: 'Prompt node input',
|
117 |
[ClapperComfyUiInputIds.NEGATIVE_PROMPT]: 'Negative prompt node input',
|
|
|
121 |
[ClapperComfyUiInputIds.IMAGE]: 'Image node input',
|
122 |
[ClapperComfyUiInputIds.OUTPUT]: 'Output node',
|
123 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|