jbilcke-hf HF staff commited on
Commit
490fe73
1 Parent(s): 5a6c0fc
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .nvmrc +1 -1
  2. README.md +1 -1
  3. bun.lockb +0 -0
  4. documentation/RELEASE.md +16 -0
  5. package.json +4 -4
  6. packages/api-client/.gitignore +0 -177
  7. packages/api-client/.npmignore +0 -4
  8. packages/api-client/.prettierrc.json +0 -9
  9. packages/api-client/LICENSE.md +0 -21
  10. packages/api-client/README.md +0 -113
  11. packages/api-client/package.json +0 -46
  12. packages/api-client/src/api/createClap.ts +0 -43
  13. packages/api-client/src/api/editClapDialogues.ts +0 -61
  14. packages/api-client/src/api/editClapEntities.ts +0 -75
  15. packages/api-client/src/api/editClapMusic.ts +0 -60
  16. packages/api-client/src/api/editClapSounds.ts +0 -60
  17. packages/api-client/src/api/editClapStory.ts +0 -114
  18. packages/api-client/src/api/editClapStoryboards.ts +0 -60
  19. packages/api-client/src/api/editClapVideos.ts +0 -85
  20. packages/api-client/src/api/exportClapToVideo.ts +0 -60
  21. packages/api-client/src/api/index.ts +0 -9
  22. packages/api-client/src/constants/config.ts +0 -16
  23. packages/api-client/src/constants/defaultValues.ts +0 -10
  24. packages/api-client/src/constants/index.ts +0 -17
  25. packages/api-client/src/constants/types.ts +0 -25
  26. packages/api-client/src/index.ts +0 -28
  27. packages/api-client/src/parsers/index.ts +0 -3
  28. packages/api-client/src/parsers/parseEntityPrompt.ts +0 -30
  29. packages/api-client/src/parsers/parseString.ts +0 -7
  30. packages/api-client/src/parsers/parseStringArray.ts +0 -9
  31. packages/api-client/src/utils/applyClapCompletion.ts +0 -21
  32. packages/api-client/src/utils/index.ts +0 -1
  33. packages/api-client/tsconfig.json +0 -31
  34. packages/api-client/tsconfig.types.json +0 -13
  35. packages/app/.nvmrc +1 -1
  36. packages/app/package.json +19 -18
  37. packages/app/src/app/api/resolve/providers/aitube/index.ts +1 -1
  38. packages/app/src/app/api/resolve/providers/comfy-replicate/index.ts +1 -1
  39. packages/app/src/app/api/resolve/providers/comfy-replicate/runWorkflow.ts +5 -1
  40. packages/app/src/app/api/resolve/providers/comfyui/convertComfyUiWorkflowApiToClapWorkflow.ts +60 -0
  41. packages/app/src/app/api/resolve/providers/comfyui/createPromptBuilder.spec.ts +595 -0
  42. packages/app/src/app/api/resolve/providers/comfyui/createPromptBuilder.ts +42 -0
  43. packages/app/src/app/api/resolve/providers/comfyui/getInputsFromComfyUiWorkflow.ts +106 -0
  44. packages/app/src/app/api/resolve/providers/comfyui/getMainInputIdsByClapWorkflowCategory.ts +29 -0
  45. packages/app/src/app/api/resolve/providers/comfyui/getMainInputsFromComfyUiWorkflow.ts +259 -0
  46. packages/app/src/app/api/resolve/providers/comfyui/graph.ts +411 -0
  47. packages/app/src/app/api/resolve/providers/comfyui/index.ts +7 -6
  48. packages/app/src/app/api/resolve/providers/comfyui/{utils.spec.ts → tests.spec.ts} +3 -3
  49. packages/app/src/app/api/resolve/providers/comfyui/types.ts +39 -0
  50. packages/app/src/app/api/resolve/providers/comfyui/utils.ts +2 -905
.nvmrc CHANGED
@@ -1 +1 @@
1
- v20.15.1
 
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.15.1`.
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:api-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:api-client": "bun run --cwd packages/api-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,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/api-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,7 +34,7 @@
34
  "workspaces": [
35
  "packages/clap",
36
  "packages/timeline",
37
- "packages/api-client",
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.15.1
 
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/api-client": "workspace:*",
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.0",
48
  "@huggingface/hub": "^0.15.1",
49
  "@huggingface/inference": "^2.8.0",
50
- "@huggingface/transformers": "3.0.0-alpha.5",
51
- "@langchain/anthropic": "^0.2.14",
52
  "@langchain/cohere": "^0.2.2",
53
- "@langchain/core": "^0.2.23",
54
- "@langchain/google-vertexai": "^0.0.25",
55
- "@langchain/groq": "^0.0.16",
56
- "@langchain/mistralai": "^0.0.28",
57
- "@langchain/openai": "^0.2.6",
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": "^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.0.3",
90
- "autoprefixer": "10.4.19",
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.1.7",
103
  "fs-extra": "^11.2.0",
104
  "is-hotkey": "^0.2.0",
105
- "lucide-react": "^0.396.0",
106
  "mediainfo.js": "^0.3.2",
107
  "mlt-xml": "^2.0.2",
108
- "monaco-editor": "^0.50.0",
109
- "next": "^14.2.5",
110
  "next-themes": "^0.3.0",
111
  "pngjs": "^7.0.0",
112
- "qs": "^6.12.1",
113
- "query-string": "^9.0.0",
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/api-client'
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
- import { getComfyWorkflow } from '../../../../../services/editors/workflow-editor/workflows/comfyui/getComfyWorkflow'
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
- ClapperComfyUiInputIds,
13
- ComfyUIWorkflowApiGraph,
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
- export enum ClapperComfyUiInputIds {
15
- PROMPT = '@clapper/prompt',
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
  }