Compare commits

..

57 Commits

Author SHA1 Message Date
243f107ef5 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-10-13 17:10:11 +02:00
9ad4e7972b continuouly image modification 2025-10-13 17:09:58 +02:00
6eec0890a7 save changes 2025-10-10 15:10:26 +02:00
8654d160b6 save changes 2025-10-06 13:12:03 +02:00
4452508dd4 save changes 2025-10-06 00:02:50 +02:00
1697523000 save changes 2025-10-05 15:01:06 +02:00
eee4e6523e save changes 2025-10-05 11:56:31 +02:00
150992aaac photo download 2025-10-02 07:37:41 +02:00
80ddafc4e6 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-10-02 07:35:44 +02:00
72a995f483 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-10-02 07:35:43 +02:00
1de8749195 save changes 2025-10-02 07:34:50 +02:00
ad8ca7b997 save changes 2025-10-02 07:34:10 +02:00
bdca42e821 save changes 2025-10-01 07:53:50 +02:00
4bce4388f9 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-26 20:55:52 +02:00
e670975ee9 sace changes 2025-09-26 20:55:43 +02:00
b56d7e69dd vton 2025-09-26 20:53:04 +02:00
d362f9e0ca Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-25 21:49:49 +02:00
28aa838937 save changes 2025-09-25 21:49:44 +02:00
4ccfd9ef3a cloth_extractor 2025-09-25 21:44:12 +02:00
39ea45fade Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-25 07:48:11 +02:00
eff12f546e face generator 2025-09-25 07:47:58 +02:00
37aba1b7d5 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-23 20:21:00 +02:00
f0cfc20db9 save changes 2025-09-23 20:20:51 +02:00
d74ad1b034 new musicspot_generator/v2 files 2025-09-23 20:19:59 +02:00
5d1cbf8c09 update folder structure for musicspot_generator 2025-09-23 15:39:26 +02:00
5d0ccc81b6 fix fgrame rate to 24 2025-09-23 04:37:40 +02:00
83588c69f5 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-22 22:23:15 +02:00
20262bc60a save changes 2025-09-22 22:23:09 +02:00
78584ce1b6 save changes 2025-09-22 22:22:57 +02:00
e5a65114d1 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-22 18:12:27 +02:00
b64d5aa6ed save changes 2025-09-22 18:12:18 +02:00
3b9ebd0325 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-22 17:15:11 +02:00
8532a33988 save changes 2025-09-22 17:15:05 +02:00
f63395fca3 save changes 2025-09-22 17:11:25 +02:00
c7279b4e8b Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-09-17 20:00:19 +02:00
d68c44de99 save changes 2025-09-17 20:00:13 +02:00
f8adaf050e save changes 2025-09-17 19:59:53 +02:00
b153826a0d save changes 2025-09-15 07:39:50 +02:00
0521b28626 save changes 2025-09-10 12:59:50 +02:00
9e056b752d save current changes 2025-08-30 23:22:11 +02:00
62e22e4965 save changes 2025-08-29 11:21:46 +02:00
153e50a356 save changes 2025-08-27 12:07:51 +02:00
d4028e8f7f save latest changes 2025-08-27 11:17:34 +02:00
3d239d039b update video size 2025-08-23 22:20:23 +02:00
6aa51c14a2 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-08-23 22:17:36 +02:00
37baaf72ab save changes 2025-08-23 22:17:27 +02:00
38fcfa4e98 save changes 2025-08-23 12:32:30 +02:00
fcd1df0102 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-08-23 10:23:06 +02:00
b25f6990d9 save changes 2025-08-23 10:23:00 +02:00
fa494f9b14 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-08-23 10:22:50 +02:00
39b2e792e7 fix model name 2025-08-23 10:22:43 +02:00
9ccde2bc74 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-08-23 10:17:51 +02:00
6031141113 save changes 2025-08-23 10:17:46 +02:00
55f83bfbaa Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-08-23 10:17:34 +02:00
1c03b862fc update image model to kres 2025-08-23 10:17:00 +02:00
1cf23caac6 Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker 2025-08-22 22:42:16 +02:00
b04fc14a6f update the logic to generate video on t2v 2025-08-22 22:42:03 +02:00
95 changed files with 33815 additions and 255 deletions

18
.clinerules/generators.md Normal file
View File

@ -0,0 +1,18 @@
#Image and Video generation
## Simple image generation with prompt
Use this file src\lib\image-generator.ts
## Image generation with existing image
Use this file src\lib\image-generator-face.ts
## Image converting
Use this file src\lib\image-converter.ts
## Video generation with static image
Use this ilfe src\lib\video-generator.ts
Everything when generator need to use existing image you have to copy to input folder to the server and use only filename.
For example if
inputfolderFullpath = SERVER1_COMFY_OUTPUT_DIR.replece("output","input");
Then copy image to the path, and pass only filename to generator function

10
.clinerules/lib.md Normal file
View File

@ -0,0 +1,10 @@
# Library Functions
## PNG Metadata
Use this file `src/lib/util.ts` for embedding and reading JSON data from PNG files.
### Embed JSON to PNG
Use this method `embedJsonToPng(path, obj)`
### Read JSON from PNG
Use this method `readJsonToPng(path)`

26
.clinerules/llm.md Normal file
View File

@ -0,0 +1,26 @@
# LLM API
## LMstudio
Use this file src\lib\lmstudio.ts
- async function callLmstudio(prompt: string): Promise<any> {
for just run prompt
- async function callLMStudioAPIWithFile(imagePath: string, prompt: string): Promise<any> {
for send file to llm
## OpenAI
Use this file src\lib\openai.ts
- async function callOpenAI(prompt: string): Promise<any>
for just run prompt
- async function callOpenAIWithFile(imagePath: string, prompt: string): Promise<any>
for send file to llm
Please construct prompt to return json alswasy for calling llm api to generate text.
If nothing specified add following instructin in the given prompt
Return the result in this forket
{"result":""}
Then extract the result param in program you generate, don't change the original function

9
.clinerules/pinterest.md Normal file
View File

@ -0,0 +1,9 @@
# Pinterest operations
Use this file src\lib\pinterest.ts for pinterest operation.
You can modify if needed.
# Get PinId from Keyword
Use this method getPinUrlFromPinterest
# Get image from pinId
Use this method downloadImageFromPin

3
.gitignore vendored
View File

@ -21,4 +21,5 @@ yarn-error.log*
# Downloaded images # Downloaded images
/download/ /download/
/generated/ /generated/
.env .env
input/

633
package-lock.json generated
View File

@ -10,14 +10,27 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/axios": "^0.14.4", "@types/axios": "^0.14.4",
"@types/fs-extra": "^11.0.4",
"@types/pngjs": "^6.0.5",
"@types/sharp": "^0.32.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"fs-extra": "^11.3.2",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"open": "^10.2.0", "open": "^10.2.0",
"puppeteer": "^24.16.2" "png-chunk-text": "^1.0.0",
"png-chunks-encode": "^1.0.0",
"png-chunks-extract": "^1.0.0",
"pngjs": "^7.0.0",
"puppeteer": "^24.16.2",
"sharp": "^0.34.4",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.19.19",
"@types/png-chunk-text": "^1.0.3",
"@types/png-chunks-encode": "^1.0.2",
"@types/png-chunks-extract": "^1.0.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.0.0" "typescript": "^5.0.0"
} }
@ -55,6 +68,419 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz",
"integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz",
"integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.3"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz",
"integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz",
"integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz",
"integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz",
"integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz",
"integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz",
"integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz",
"integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz",
"integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz",
"integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz",
"integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.3"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz",
"integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz",
"integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.3"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz",
"integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.3"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
"integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.3"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz",
"integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz",
"integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.3"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz",
"integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.5.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz",
"integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz",
"integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz",
"integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@ -138,15 +564,66 @@
"axios": "*" "axios": "*"
} }
}, },
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.11", "version": "20.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==",
"devOptional": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/png-chunk-text": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/png-chunk-text/-/png-chunk-text-1.0.3.tgz",
"integrity": "sha512-7keEFz73uNJ9Ar1XMCNnHEXT9pICJnouMQCCYgBEmHMgdkXaQzSTmSvr6tUDSqgdEgmlRAxZd97wprgliyZoCg==",
"dev": true
},
"node_modules/@types/png-chunks-encode": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/png-chunks-encode/-/png-chunks-encode-1.0.2.tgz",
"integrity": "sha512-Dxn0aXEcSg1wVeHjvNlygm/+fKBDzWMCdxJYhjGUTeefFW/jYxWcrg+W7ppLBfH44iJMqeVBHtHBwtYQUeYvgw==",
"dev": true
},
"node_modules/@types/png-chunks-extract": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/png-chunks-extract/-/png-chunks-extract-1.0.2.tgz",
"integrity": "sha512-z6djfFIbrrddtunoMJBOPlyZrnmeuG1kkvHUNi2QfpOb+JMMLuLliHHTmMyRi7k7LiTAut0HbdGCF6ibDtQAHQ==",
"dev": true
},
"node_modules/@types/pngjs": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/sharp": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.32.0.tgz",
"integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
"deprecated": "This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.",
"dependencies": {
"sharp": "*"
}
},
"node_modules/@types/yauzl": { "node_modules/@types/yauzl": {
"version": "2.10.3", "version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@ -454,6 +931,14 @@
} }
} }
}, },
"node_modules/crc-32": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz",
"integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/create-require": { "node_modules/create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@ -550,6 +1035,14 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==",
"engines": {
"node": ">=8"
}
},
"node_modules/devtools-protocol": { "node_modules/devtools-protocol": {
"version": "0.0.1475386", "version": "0.0.1475386",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz",
@ -780,6 +1273,19 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fs-extra": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -877,6 +1383,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -1055,6 +1566,17 @@
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
}, },
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -1261,6 +1783,36 @@
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"node_modules/png-chunk-text": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz",
"integrity": "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="
},
"node_modules/png-chunks-encode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-chunks-encode/-/png-chunks-encode-1.0.0.tgz",
"integrity": "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==",
"dependencies": {
"crc-32": "^0.3.0",
"sliced": "^1.0.1"
}
},
"node_modules/png-chunks-extract": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz",
"integrity": "sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==",
"dependencies": {
"crc-32": "^0.3.0"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -1385,6 +1937,52 @@
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
}, },
"node_modules/sharp": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
"integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==",
"hasInstallScript": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.0",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.4",
"@img/sharp-darwin-x64": "0.34.4",
"@img/sharp-libvips-darwin-arm64": "1.2.3",
"@img/sharp-libvips-darwin-x64": "1.2.3",
"@img/sharp-libvips-linux-arm": "1.2.3",
"@img/sharp-libvips-linux-arm64": "1.2.3",
"@img/sharp-libvips-linux-ppc64": "1.2.3",
"@img/sharp-libvips-linux-s390x": "1.2.3",
"@img/sharp-libvips-linux-x64": "1.2.3",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.3",
"@img/sharp-libvips-linuxmusl-x64": "1.2.3",
"@img/sharp-linux-arm": "0.34.4",
"@img/sharp-linux-arm64": "0.34.4",
"@img/sharp-linux-ppc64": "0.34.4",
"@img/sharp-linux-s390x": "0.34.4",
"@img/sharp-linux-x64": "0.34.4",
"@img/sharp-linuxmusl-arm64": "0.34.4",
"@img/sharp-linuxmusl-x64": "0.34.4",
"@img/sharp-wasm32": "0.34.4",
"@img/sharp-win32-arm64": "0.34.4",
"@img/sharp-win32-ia32": "0.34.4",
"@img/sharp-win32-x64": "0.34.4"
}
},
"node_modules/sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="
},
"node_modules/smart-buffer": { "node_modules/smart-buffer": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -1573,8 +2171,27 @@
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
"devOptional": true },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/esm/bin/uuid"
}
}, },
"node_modules/v8-compile-cache-lib": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",

View File

@ -6,24 +6,40 @@
"scripts": { "scripts": {
"start": "tsc && node dist/index.js", "start": "tsc && node dist/index.js",
"build": "tsc", "build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"db:schema": "ts-node src/schema.ts", "db:schema": "ts-node src/schema.ts",
"db:test": "ts-node src/testmysql.ts" "db:test": "ts-node src/testmysql.ts",
"infinity:start": "ts-node src/infinityvideo_generator/start.ts",
"convert:pinterest-face": "ts-node src/imageconverter/pinterest_face_portrait.ts",
"tool:generate-video-from-input": "ts-node src/tools/generateVideoFromInput.ts"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.19.19",
"@types/png-chunk-text": "^1.0.3",
"@types/png-chunks-encode": "^1.0.2",
"@types/png-chunks-extract": "^1.0.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"@types/axios": "^0.14.4", "@types/axios": "^0.14.4",
"@types/fs-extra": "^11.0.4",
"@types/pngjs": "^6.0.5",
"@types/sharp": "^0.32.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"fs-extra": "^11.3.2",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"open": "^10.2.0", "open": "^10.2.0",
"puppeteer": "^24.16.2" "png-chunk-text": "^1.0.0",
"png-chunks-encode": "^1.0.0",
"png-chunks-extract": "^1.0.0",
"pngjs": "^7.0.0",
"puppeteer": "^24.16.2",
"sharp": "^0.34.4",
"uuid": "^11.1.0"
} }
} }

View File

@ -0,0 +1,468 @@
import { callOpenAI, callOpenAIWithFile } from './lib/openai';
import { generateVideo } from './lib/video-generator';
import { generateImage as generateImageMixStyle } from './lib/image-generator-mix-style';
import { generateImage as generateImage } from './lib/image-generator';
import { logger } from './lib/logger';
import * as fs from 'fs/promises';
import dotenv from 'dotenv';
import path from 'path';
import puppeteer from 'puppeteer';
import { VideoModel } from './lib/db/video';
dotenv.config();
const RUN_ONCE = (process.env.RUN_ONCE || 'false').toLowerCase() === 'true';
const NUMBER_OF_KEYWORDS = Number(process.env.NUMBER_OF_KEYWORDS) || 20;
const SCROLL_SEARCH = Number(process.env.SCROLL_SEARCH) || 5; // scroll times on search results
const SCROLL_PIN = Number(process.env.SCROLL_PIN) || 3; // scroll times on pin page
const USE_REFERENCE_IMAGE = (process.env.USE_REFERENCE_IMAGE || 'true').toLowerCase() === 'true';
// Hard-coded user prompt (used as the video generation instruction).
// You can change this string here or set a different value if you edit the file.
const HARDCODED_USER_PROMPT = process.env.HARDCODED_USER_PROMPT || "Generate 20 dance keywords more something like street dance. So I can search pinterest.";
const servers = [
/*{
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},*/
{
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
},
].filter((s): s is { baseUrl: string; outputDir: string } => !!s.baseUrl && !!s.outputDir);
interface PipelineItem {
keyword: string;
pinUrl: string;
imagePrompt: string;
videoPrompt: string;
baseImagePath: string; // downloaded from pin
generatedImagePath?: string; // generated on server
}
// Re-usable helper to extract JSON embedded in text
function extractJsonFromText(text: string): any | null {
if (!text || typeof text !== 'string') return null;
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (fenced && fenced[1]) {
try { return JSON.parse(fenced[1].trim()); } catch (e) { /* fall through */ }
}
const brace = text.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
if (brace && brace[0]) {
try { return JSON.parse(brace[0]); } catch (e) { return null; }
}
// Attempt line-separated keywords fallback
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
if (lines.length > 1) return lines;
return null;
}
// Wrapper to call OpenAI with an image and prompt and extract JSON-like result
async function callOpenAIWithFileAndExtract(imagePath: string, prompt: string, maxRetries = 5): Promise<any | null> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await callOpenAIWithFile(imagePath, prompt);
if (!res) {
logger.warn(`callOpenAIWithFileAndExtract attempt ${attempt} returned empty response`);
continue;
}
if (typeof res === 'object') return res;
if (typeof res === 'string') {
const parsed = extractJsonFromText(res);
if (parsed) return parsed;
}
logger.warn(`callOpenAIWithFileAndExtract: attempt ${attempt} unexpected shape`);
} catch (err) {
logger.warn(`callOpenAIWithFileAndExtract: attempt ${attempt} failed: ${err}`);
}
}
logger.error(`callOpenAIWithFileAndExtract: failed after ${maxRetries} attempts`);
return null;
}
// Ask ChatGPT to produce keywords from a single high-level prompt
async function generateKeywordsFromPrompt(prompt: string, count = NUMBER_OF_KEYWORDS): Promise<string[]> {
const instruction = `You are given a short instruction describing the type of short 8-second cinematic videos to create.
Return exactly a JSON array of ${count} short keyword phrases (each 1-3 words) suitable for searching Pinterest. Example output: ["sunset beach","city skyline",...]. Do not include commentary.`;
const res = await callOpenAI(`${instruction}\n\nInstruction: ${prompt}`);
const parsed = extractJsonFromText(typeof res === 'string' ? res : (res && (res.text || JSON.stringify(res))));
if (Array.isArray(parsed)) {
return parsed.map(String).slice(0, count);
}
// fallback: try to parse common fields
if (res && typeof res === 'object') {
const maybe = res.keywords || res.list || res.items || res.keywords_list;
if (Array.isArray(maybe)) return maybe.map(String).slice(0, count);
}
// last fallback: split lines
const text = typeof res === 'string' ? res : JSON.stringify(res);
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
if (lines.length >= 1) {
// extract up to count tokens (remove numbering)
const cleaned = lines.map(l => l.replace(/^\d+[\).\s-]*/, '').trim()).filter(Boolean);
return cleaned.slice(0, count);
}
return [];
}
async function getPinUrlFromPinterest(keyword: string, scrollCount = SCROLL_SEARCH): Promise<string | null> {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
const searchUrl = `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(keyword)}`;
await page.goto(searchUrl, { waitUntil: 'networkidle2' });
for (let i = 0; i < scrollCount; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise(r => setTimeout(r, 500 + Math.random() * 1000));
}
const pinLinks = await page.$$eval('a', (anchors) =>
anchors.map((a) => a.href).filter((href) => href.includes('/pin/'))
);
if (pinLinks.length > 0) return pinLinks[Math.floor(Math.random() * pinLinks.length)];
return null;
} catch (error) {
logger.error('Error while getting pin URL from Pinterest:', error);
return null;
} finally {
await browser.close();
}
}
// Download one high-quality image from a pin page
async function downloadOneImageFromPin(pinUrl: string, count: number = 1, scrollTimes = SCROLL_PIN): Promise<string[]> {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
await page.goto(pinUrl, { waitUntil: 'networkidle2', timeout: 30000 });
for (let i = 0; i < scrollTimes; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise((r) => setTimeout(r, 700 + Math.random() * 800));
}
const imgs: string[] = await page.$$eval('img', imgs => {
const urls: string[] = imgs.map(img => {
const srcset = (img as HTMLImageElement).getAttribute('srcset') || '';
if (!srcset) return '';
const parts = srcset.split(',').map(p => p.trim());
for (const part of parts) {
const m = part.match(/^(\S+)\s+4x$/);
if (m && m[1]) return m[1];
}
const src = (img as HTMLImageElement).src || '';
if (src.includes('/originals/')) return src;
return '';
}).filter(s => !!s && s.includes('pinimg'));
return urls;
});
if (!imgs || imgs.length === 0) {
logger.warn(`No high-res images found on pin ${pinUrl}`);
return [];
}
const shuffled = imgs.slice().sort(() => 0.5 - Math.random());
const chosen = shuffled.slice(0, Math.min(count, shuffled.length));
const outDir = path.join(process.cwd(), 'download');
await fs.mkdir(outDir, { recursive: true });
const results: string[] = [];
for (let i = 0; i < chosen.length; i++) {
const src = chosen[i];
try {
const imgPage = await browser.newPage();
const resp = await imgPage.goto(src, { timeout: 30000, waitUntil: 'networkidle2' });
if (!resp) { await imgPage.close(); continue; }
const buffer = await resp.buffer();
const pinId = pinUrl.split('/').filter(Boolean).pop() || `pin_${Date.now()}`;
const timestamp = Date.now();
const outPath = path.join(outDir, `${pinId}_${timestamp}_${i}.png`);
await fs.writeFile(outPath, buffer);
results.push(outPath);
await imgPage.close();
} catch (err) {
logger.error(`Failed to download image ${src} from ${pinUrl}:`, err);
}
}
return results;
} catch (err) {
logger.error(`Failed to download images from ${pinUrl}:`, err);
return [];
} finally {
await browser.close();
}
}
// Reuse the getPromptsForImage logic (uses OpenAI + image)
async function getPromptsForImage(imagePaths: string[], pinUrl: string, genrePrompt: string): Promise<{ imagePrompt: string; videoPrompt: string; baseImagePath: string } | null> {
const pinId = pinUrl.split('/').filter(Boolean).pop() || `pin_${Date.now()}`;
const timestamp = Date.now();
const renamedImagePaths: string[] = [];
for (let i = 0; i < imagePaths.length; i++) {
const renamedPath = path.join(path.dirname(imagePaths[i]), `${pinId}_${timestamp}_${i}.png`);
await fs.rename(imagePaths[i], renamedPath);
renamedImagePaths.push(renamedPath);
}
const imageForPrompt = renamedImagePaths[Math.floor(Math.random() * renamedImagePaths.length)];
try {
const step1Prompt = `Return exactly one JSON object: { "mainobject": "..." }. Look at the provided image and determine the single most prominent/main object or subject in the scene. Answer with a short noun or short phrase.`;
const step1Res = await callOpenAIWithFileAndExtract(imageForPrompt, step1Prompt, 5);
const mainobject = (step1Res && (step1Res.mainobject || step1Res.mainObject || step1Res.object)) ? String(step1Res.mainobject || step1Res.mainObject || step1Res.object).trim() : '';
if (!mainobject) throw new Error('Could not detect main object');
const step2Prompt = `You have access to the image and the detected main object: "${mainobject}". Decide which single action type best fits this scene from the list: - no action - micro animation - big movement - impossible movement - Dance (if portrait). Return exactly one JSON object: { "actiontype": "..." }.`;
const step2Res = await callOpenAIWithFileAndExtract(imageForPrompt, step2Prompt, 5);
const actiontype = (step2Res && (step2Res.actiontype || step2Res.actionType)) ? String(step2Res.actiontype || step2Res.actionType).trim() : '';
const step3Prompt = `Given the image and the following information: - main object: "${mainobject}" - chosen action type: "${actiontype}" From the options pick the single best camera approach: - static camera - pan - rotation - follow the moving object - zoom to the object - impossible camera work. Return exactly one JSON object: { "cameraworkType": "..." }.`;
const step3Res = await callOpenAIWithFileAndExtract(imageForPrompt, step3Prompt, 5);
const cameraworkType = (step3Res && (step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera)) ? String(step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera).trim() : '';
const finalPrompt = `Return exactly one JSON object: { "scene": "...", "action":"...", "camera":"...", "image_prompt":"...", "videoPrompt":"..." } and nothing else.
Write "videoPrompt" in 100150 words, present tense, plain concrete language.
Write "image_prompt" as a concise, detailed prompt suitable for generating a similar image.
Here is information of the scene:
Detected Main Object: ${mainobject}
Suggested Action Type: ${actiontype}
Suggested Camera Work: ${cameraworkType}
Genre instruction: ${genrePrompt}`;
const finalRes = await callOpenAIWithFileAndExtract(imageForPrompt, finalPrompt, 5);
const imagePrompt = finalRes && (finalRes.image_prompt || finalRes.imagePrompt || finalRes.image_prompt) ? String(finalRes.image_prompt || finalRes.imagePrompt) : '';
const videoPrompt = finalRes && (finalRes.videoPrompt || finalRes.video_prompt || finalRes.video_prompt) ? String(finalRes.videoPrompt || finalRes.video_prompt) : '';
if (!imagePrompt || !videoPrompt) throw new Error('Final LM output missing prompts');
return { imagePrompt, videoPrompt, baseImagePath: imageForPrompt };
} catch (error) {
logger.error('Failed to get prompts for image:', error);
for (const p of renamedImagePaths) {
try { await fs.unlink(p); } catch (e) { /* ignore */ }
}
return null;
}
}
async function generateImageForItem(item: PipelineItem, server: { baseUrl: string; outputDir: string; }): Promise<string | null> {
const { imagePrompt, baseImagePath } = item as any;
const { baseUrl, outputDir } = server;
const inputDir = outputDir.replace("output", "input");
const sourceFileNames: string[] = [];
try {
if (USE_REFERENCE_IMAGE) {
const fileName = path.basename(baseImagePath);
const destPath = path.join(inputDir, fileName);
await fs.copyFile(baseImagePath, destPath);
sourceFileNames.push(fileName);
logger.info(`Copied ${baseImagePath} to ${destPath}`);
const srcA = sourceFileNames[0];
const srcB = sourceFileNames[1] || sourceFileNames[0];
const generatedImagePath = await generateImageMixStyle(
imagePrompt,
srcA,
srcB,
`${path.basename(baseImagePath)}`,
baseUrl,
outputDir,
{ width: 1280, height: 720 }
);
return generatedImagePath;
} else {
const generatedImagePath = await generateImage(
imagePrompt,
`${path.basename(baseImagePath)}`,
baseUrl,
outputDir,
'qwen',
{ width: 1280, height: 720 }
);
return generatedImagePath;
}
} catch (error) {
logger.error(`Failed to generate image on server ${baseUrl}:`, error);
return null;
} finally {
// cleanup base image copied to server input
for (const fileName of sourceFileNames) {
try {
const serverPath = path.join(inputDir, fileName);
await fs.unlink(serverPath);
} catch (error) {
logger.error(`Failed to delete server image ${fileName}:`, error);
}
}
// local base image cleanup is left to caller if desired
}
}
(async () => {
// Entry prompt: use hard-coded prompt defined at the top of the file
const userPrompt = HARDCODED_USER_PROMPT;
if (servers.length === 0) {
logger.error("No servers configured. Please set SERVER1_COMFY_BASE_URL/OUTPUT_DIR etc in .env");
return;
}
while (true) {
logger.info(`Starting pipeline iteration for prompt: ${userPrompt}`);
// 1) Ask OpenAI to generate keywords
const keywords = await generateKeywordsFromPrompt(userPrompt, NUMBER_OF_KEYWORDS);
logger.info(`Generated ${keywords.length} keywords: ${keywords.join(', ')}`);
// 2) For each keyword: search pinterest, pick pinId, open pin page, pick one photo, generate prompts, generate image on servers
const pipelineItems: PipelineItem[] = [];
for (const kw of keywords) {
try {
const pinUrl = await getPinUrlFromPinterest(kw, SCROLL_SEARCH);
if (!pinUrl) {
logger.warn(`No pin found for keyword "${kw}"`);
continue;
}
const downloaded = await downloadOneImageFromPin(pinUrl, 1, SCROLL_PIN);
if (!downloaded || downloaded.length === 0) {
logger.warn(`No photo downloaded for pin ${pinUrl}`);
continue;
}
const prompts = await getPromptsForImage(downloaded, pinUrl, kw);
if (!prompts) {
logger.warn(`Failed to produce prompts for image from pin ${pinUrl}`);
// cleanup downloaded file
for (const f of downloaded) {
try { await fs.unlink(f); } catch (e) { /* ignore */ }
}
continue;
}
const item: PipelineItem = {
keyword: kw,
pinUrl,
imagePrompt: prompts.imagePrompt,
videoPrompt: prompts.videoPrompt,
baseImagePath: prompts.baseImagePath,
};
pipelineItems.push(item);
logger.info(`Prepared pipeline item for keyword "${kw}"`);
} catch (err) {
logger.error(`Error processing keyword ${kw}:`, err);
}
}
// 3) Generate images for all pipeline items, distributed across servers concurrently
logger.info(`Starting image generation for ${pipelineItems.length} items`);
if (pipelineItems.length > 0) {
const tasksByServer: PipelineItem[][] = servers.map(() => []);
pipelineItems.forEach((it, idx) => {
const si = idx % servers.length;
tasksByServer[si].push(it);
});
await Promise.all(servers.map(async (server, si) => {
const tasks = tasksByServer[si];
if (!tasks || tasks.length === 0) return;
logger.info(`Server ${server.baseUrl} generating ${tasks.length} images`);
const results = await Promise.all(tasks.map(t => generateImageForItem(t, server)));
for (let i = 0; i < tasks.length; i++) {
const res = results[i];
if (res) tasks[i].generatedImagePath = res;
}
logger.info(`Server ${server.baseUrl} finished image generation`);
}));
}
// 4) Collect successful items and generate videos (distributed across servers concurrently)
const readyItems = pipelineItems.filter(i => i.generatedImagePath);
logger.info(`Starting video generation for ${readyItems.length} items`);
if (readyItems.length > 0) {
const tasksByServer: PipelineItem[][] = servers.map(() => []);
readyItems.forEach((it, idx) => {
const si = idx % servers.length;
tasksByServer[si].push(it);
});
await Promise.all(servers.map(async (server, si) => {
const tasks = tasksByServer[si];
if (!tasks || tasks.length === 0) return;
logger.info(`Server ${server.baseUrl} starting ${tasks.length} video task(s)`);
await Promise.allSettled(tasks.map(async (task) => {
if (!task.generatedImagePath) {
logger.warn(`Skipping a task on ${server.baseUrl} - missing generatedImagePath`);
return;
}
const inputDir = server.outputDir.replace("output", "input");
const generatedImageName = path.basename(task.generatedImagePath);
const serverImagePath = path.join(inputDir, generatedImageName);
try {
await fs.copyFile(task.generatedImagePath, serverImagePath);
logger.info(`Copied ${task.generatedImagePath} to ${serverImagePath}`);
const videoFileName = `${path.basename(task.generatedImagePath, path.extname(task.generatedImagePath))}.mp4`;
const videoPath = await generateVideo(
task.videoPrompt,
generatedImageName,
videoFileName,
server.baseUrl,
server.outputDir,
{ width: 1280, height: 720 },
false,
false
);
if (videoPath) {
const videoData = {
genre: task.keyword,
sub_genre: task.keyword,
scene: '',
action: '',
camera: '',
image_prompt: task.imagePrompt,
video_prompt: task.videoPrompt,
image_path: task.generatedImagePath,
video_path: videoPath,
};
// ensure image_path is string (guard above)
const videoId = await VideoModel.create(videoData);
logger.info(`Saved video record ID: ${videoId}`);
const newImageName = `${videoId}_${task.keyword}${path.extname(task.generatedImagePath)}`;
const newVideoName = `${videoId}_${task.keyword}${path.extname(videoPath)}`;
const newImagePath = path.join(path.dirname(task.generatedImagePath), newImageName);
const newVideoPath = path.join(path.dirname(videoPath), newVideoName);
await fs.rename(task.generatedImagePath, newImagePath);
await fs.rename(videoPath, newVideoPath);
await VideoModel.update(videoId, {
image_path: newImagePath,
video_path: newVideoPath,
});
logger.info(`Renamed and updated DB for video ID: ${videoId}`);
} else {
logger.warn(`Video generation returned no path for ${task.generatedImagePath} on ${server.baseUrl}`);
}
} catch (err) {
logger.error('Error during video generation pipeline step:', err);
} finally {
try { await fs.unlink(serverImagePath); } catch (e) { /* ignore */ }
}
}));
logger.info(`Server ${server.baseUrl} finished video tasks`);
}));
}
logger.info('Pipeline iteration finished.');
// Cleanup base images downloaded from pins if you want to remove them.
for (const item of pipelineItems) {
try { await fs.unlink(item.baseImagePath); } catch (e) { /* ignore */ }
}
if (RUN_ONCE) {
logger.info('RUN_ONCE=true - exiting after one iteration');
return;
}
}
})();

View File

@ -0,0 +1,207 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 1088883674457465,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"66",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"11",
6
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": true,
"enable_vl_resize": true,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"24",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"20": {
"inputs": {
"filename_prefix": "qwenedit",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "extract the outfit onto a white background"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"24": {
"inputs": {
"measurement": "pixels",
"width": 720,
"height": 1280,
"fit": "contain",
"method": "nearest-exact",
"image": [
"64",
0
]
},
"class_type": "Image Resize (rgthree)",
"_meta": {
"title": "Image Resize (rgthree)"
}
},
"64": {
"inputs": {
"image": "3096293489212792_1758825204441_2.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"65": {
"inputs": {
"images": [
"24",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"66": {
"inputs": {
"lora_name": "extract-outfit_v3.safetensors",
"strength_model": 1,
"model": [
"4",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
}
}

View File

@ -0,0 +1,396 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 936152772258115,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"font_file": "Alibaba-PuHuiTi-Heavy.ttf",
"font_size": 40,
"border": 32,
"color_theme": "light",
"reel_1": [
"10",
0
]
},
"class_type": "LayerUtility: ImageReelComposit",
"_meta": {
"title": "LayerUtility: Image Reel Composit"
}
},
"10": {
"inputs": {
"image1_text": "Original image",
"image2_text": "Reference",
"image3_text": "Result",
"image4_text": "image4",
"reel_height": 512,
"border": 32,
"image1": [
"11",
1
],
"image2": [
"11",
2
],
"image3": [
"8",
0
]
},
"class_type": "LayerUtility: ImageReel",
"_meta": {
"title": "LayerUtility: Image Reel"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"30",
0
],
"image2": [
"27",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"14": {
"inputs": {
"image": "model_outfit_location_1760043932148.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "load base image"
}
},
"19": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_dxzmg_00211_.png&type=temp&subfolder=&rand=0.09499077981761894"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_dxzmg_00212_.png&type=temp&subfolder=&rand=0.21125213225471684"
}
]
},
"image_a": [
"11",
1
],
"image_b": [
"8",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"20": {
"inputs": {
"filename_prefix": "combined",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "请将图2中的模特处理成手持图1中包包的照片。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"22": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"9",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"23": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"24": {
"inputs": {
"vae_name": "sdxl_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"25": {
"inputs": {
"samples": [
"23",
0
],
"vae": [
"24",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"26": {
"inputs": {
"images": [
"25",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"27": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "resize",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"14",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"28": {
"inputs": {
"pixels": [
"27",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
},
"29": {
"inputs": {
"image": "handbag_1760043932148.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "load reference image"
}
},
"30": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "resize",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"29",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"31": {
"inputs": {
"Value": 720
},
"class_type": "DF_Integer",
"_meta": {
"title": "width"
}
},
"32": {
"inputs": {
"Value": 1280
},
"class_type": "DF_Integer",
"_meta": {
"title": "height"
}
}
}

View File

@ -0,0 +1,444 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 38026585691397,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"36",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"font_file": "Alibaba-PuHuiTi-Heavy.ttf",
"font_size": 40,
"border": 32,
"color_theme": "light",
"reel_1": [
"10",
0
]
},
"class_type": "LayerUtility: ImageReelComposit",
"_meta": {
"title": "LayerUtility: Image Reel Composit"
}
},
"10": {
"inputs": {
"image1_text": "Original image",
"image2_text": "Reference",
"image3_text": "Result",
"image4_text": "image4",
"reel_height": 512,
"border": 32,
"image1": [
"11",
1
],
"image2": [
"11",
2
],
"image3": [
"8",
0
]
},
"class_type": "LayerUtility: ImageReel",
"_meta": {
"title": "LayerUtility: Image Reel"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"27",
0
],
"image2": [
"33",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"14": {
"inputs": {
"image": "model_1760082843769.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "load base image"
}
},
"19": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_uoazy_00279_.png&type=temp&subfolder=&rand=0.4405150352070387"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_uoazy_00280_.png&type=temp&subfolder=&rand=0.9388629603648289"
}
]
},
"image_a": [
"11",
1
],
"image_b": [
"8",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"20": {
"inputs": {
"filename_prefix": "combined",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "以图像2为基础生成一张女性肖像照片。她穿着一件黑色薄纱长袖上衣一条光滑的皮革及膝裙和勃艮第色的尖头靴子手提一个深红色的手提包。场景改为极简主义风格的客厅摆放着中性的沙发、镜面墙饰、盆栽植物和浅色地板营造出明亮而宽敞的美感。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"22": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"9",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"23": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"24": {
"inputs": {
"vae_name": "sdxl_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"25": {
"inputs": {
"samples": [
"23",
0
],
"vae": [
"24",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"26": {
"inputs": {
"images": [
"25",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"27": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "resize",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"14",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"28": {
"inputs": {
"pixels": [
"27",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
},
"29": {
"inputs": {
"image": "pose_1760082843769.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "load reference image"
}
},
"30": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "resize",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"29",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"31": {
"inputs": {
"Value": 720
},
"class_type": "DF_Integer",
"_meta": {
"title": "width"
}
},
"32": {
"inputs": {
"Value": 1280
},
"class_type": "DF_Integer",
"_meta": {
"title": "height"
}
},
"33": {
"inputs": {
"detect_hand": "enable",
"detect_body": "enable",
"detect_face": "enable",
"resolution": 512,
"bbox_detector": "yolox_l.onnx",
"pose_estimator": "dw-ll_ucoco_384_bs5.torchscript.pt",
"scale_stick_for_xinsr_cn": "disable",
"image": [
"30",
0
]
},
"class_type": "DWPreprocessor",
"_meta": {
"title": "DWPose Estimator"
}
},
"35": {
"inputs": {
"images": [
"33",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"36": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
}
}

View File

@ -0,0 +1,396 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 323591075024702,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"font_file": "Alibaba-PuHuiTi-Heavy.ttf",
"font_size": 40,
"border": 32,
"color_theme": "light",
"reel_1": [
"10",
0
]
},
"class_type": "LayerUtility: ImageReelComposit",
"_meta": {
"title": "LayerUtility: Image Reel Composit"
}
},
"10": {
"inputs": {
"image1_text": "Original image",
"image2_text": "Reference",
"image3_text": "Result",
"image4_text": "image4",
"reel_height": 512,
"border": 32,
"image1": [
"11",
1
],
"image2": [
"11",
2
],
"image3": [
"8",
0
]
},
"class_type": "LayerUtility: ImageReel",
"_meta": {
"title": "LayerUtility: Image Reel"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": true,
"enable_vl_resize": true,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"27",
0
],
"image2": [
"30",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"14": {
"inputs": {
"image": "model_outfit_location_handbag1_1760085003312.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"19": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_uoazy_00305_.png&type=temp&subfolder=&rand=0.5408789951924671"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_uoazy_00306_.png&type=temp&subfolder=&rand=0.2425856190711294"
}
]
},
"image_a": [
"11",
1
],
"image_b": [
"8",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"20": {
"inputs": {
"filename_prefix": "combined",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "请将图2中的女性修改成把图1的包背在肩上。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"22": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"9",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"23": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"24": {
"inputs": {
"vae_name": "sdxl_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"25": {
"inputs": {
"samples": [
"23",
0
],
"vae": [
"24",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"26": {
"inputs": {
"images": [
"25",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"27": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "crop",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"14",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"28": {
"inputs": {
"pixels": [
"27",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
},
"29": {
"inputs": {
"image": "handbag_1760085003312.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"30": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "crop",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"29",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"31": {
"inputs": {
"Value": 720
},
"class_type": "DF_Integer",
"_meta": {
"title": "width"
}
},
"32": {
"inputs": {
"Value": 1280
},
"class_type": "DF_Integer",
"_meta": {
"title": "height"
}
}
}

View File

@ -0,0 +1,444 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 38026585691397,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"36",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"font_file": "Alibaba-PuHuiTi-Heavy.ttf",
"font_size": 40,
"border": 32,
"color_theme": "light",
"reel_1": [
"10",
0
]
},
"class_type": "LayerUtility: ImageReelComposit",
"_meta": {
"title": "LayerUtility: Image Reel Composit"
}
},
"10": {
"inputs": {
"image1_text": "Original image",
"image2_text": "Reference",
"image3_text": "Result",
"image4_text": "image4",
"reel_height": 512,
"border": 32,
"image1": [
"11",
1
],
"image2": [
"11",
2
],
"image3": [
"8",
0
]
},
"class_type": "LayerUtility: ImageReel",
"_meta": {
"title": "LayerUtility: Image Reel"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"27",
0
],
"image2": [
"33",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"14": {
"inputs": {
"image": "model_1760082843769.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "load base image"
}
},
"19": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_uoazy_00279_.png&type=temp&subfolder=&rand=0.4405150352070387"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_uoazy_00280_.png&type=temp&subfolder=&rand=0.9388629603648289"
}
]
},
"image_a": [
"11",
1
],
"image_b": [
"8",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"20": {
"inputs": {
"filename_prefix": "combined",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "以图像2为基础生成一张女性肖像照片。她穿着一件黑色薄纱长袖上衣一条光滑的皮革及膝裙和勃艮第色的尖头靴子手提一个深红色的手提包。场景改为极简主义风格的客厅摆放着中性的沙发、镜面墙饰、盆栽植物和浅色地板营造出明亮而宽敞的美感。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"22": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"9",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"23": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"24": {
"inputs": {
"vae_name": "sdxl_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"25": {
"inputs": {
"samples": [
"23",
0
],
"vae": [
"24",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"26": {
"inputs": {
"images": [
"25",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"27": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "resize",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"14",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"28": {
"inputs": {
"pixels": [
"27",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
},
"29": {
"inputs": {
"image": "pose_1760082843769.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "load reference image"
}
},
"30": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "resize",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"29",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"31": {
"inputs": {
"Value": 720
},
"class_type": "DF_Integer",
"_meta": {
"title": "width"
}
},
"32": {
"inputs": {
"Value": 1280
},
"class_type": "DF_Integer",
"_meta": {
"title": "height"
}
},
"33": {
"inputs": {
"detect_hand": "enable",
"detect_body": "enable",
"detect_face": "enable",
"resolution": 512,
"bbox_detector": "yolox_l.onnx",
"pose_estimator": "dw-ll_ucoco_384_bs5.torchscript.pt",
"scale_stick_for_xinsr_cn": "disable",
"image": [
"30",
0
]
},
"class_type": "DWPreprocessor",
"_meta": {
"title": "DWPose Estimator"
}
},
"35": {
"inputs": {
"images": [
"33",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"36": {
"inputs": {
"width": [
"31",
0
],
"height": [
"32",
0
],
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
}
}

View File

@ -0,0 +1,558 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 1058883705232539,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"66",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"11",
6
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"84",
0
],
"image2": [
"82",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"15": {
"inputs": {
"image": "cloth_0001.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"21": {
"inputs": {
"value": "change clothes of image1 with image2"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"64": {
"inputs": {
"image": "Lauren_body.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"66": {
"inputs": {
"lora_name": "extract-outfit_v3.safetensors",
"strength_model": 1,
"model": [
"4",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"67": {
"inputs": {
"detect_hand": "enable",
"detect_body": "enable",
"detect_face": "enable",
"resolution": 512,
"bbox_detector": "yolox_l.onnx",
"pose_estimator": "dw-ll_ucoco_384_bs5.torchscript.pt",
"scale_stick_for_xinsr_cn": "disable",
"image": [
"68",
0
]
},
"class_type": "DWPreprocessor",
"_meta": {
"title": "DWPose Estimator"
}
},
"68": {
"inputs": {
"image": "281543721672978_1758880135639_0.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"69": {
"inputs": {
"images": [
"81",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"76": {
"inputs": {
"number": 720
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"77": {
"inputs": {
"number": 1280
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"78": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"81": {
"inputs": {
"width": 480,
"height": 962,
"upscale_method": "nearest-exact",
"keep_proportion": "pad",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"67",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"82": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "crop",
"pad_color": "255,255,255",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"15",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"83": {
"inputs": {
"images": [
"82",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"84": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "pad",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"64",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"85": {
"inputs": {
"images": [
"84",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"86": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"87": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"88": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"87",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"89": {
"inputs": {
"conditioning": [
"95",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"90": {
"inputs": {
"lora_name": "extract-outfit_v3.safetensors",
"strength_model": 1,
"model": [
"88",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"91": {
"inputs": {
"seed": 416948400785889,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"90",
0
],
"positive": [
"95",
0
],
"negative": [
"89",
0
],
"latent_image": [
"95",
6
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"92": {
"inputs": {
"samples": [
"91",
0
],
"vae": [
"94",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"93": {
"inputs": {
"filename_prefix": "qwenedit",
"images": [
"92",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"94": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"95": {
"inputs": {
"prompt": [
"96",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"86",
0
],
"vae": [
"94",
0
],
"image1": [
"8",
0
],
"image2": [
"81",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"96": {
"inputs": {
"value": "change pose of image1 with image2, keep background same as image1"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"132": {
"inputs": {
"images": [
"8",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"133": {
"inputs": {
"filename_prefix": "qwenimtermediate",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"134": {
"inputs": {
"filename_prefix": "qwenpose",
"images": [
"81",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}

View File

@ -0,0 +1,333 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 639545413023960,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"font_file": "Alibaba-PuHuiTi-Heavy.ttf",
"font_size": 40,
"border": 32,
"color_theme": "light",
"reel_1": [
"10",
0
]
},
"class_type": "LayerUtility: ImageReelComposit",
"_meta": {
"title": "LayerUtility: Image Reel Composit"
}
},
"10": {
"inputs": {
"image1_text": "Original image",
"image2_text": "Reference",
"image3_text": "Result",
"image4_text": "image4",
"reel_height": 512,
"border": 32,
"image1": [
"11",
1
],
"image2": [
"11",
2
],
"image3": [
"8",
0
]
},
"class_type": "LayerUtility: ImageReel",
"_meta": {
"title": "LayerUtility: Image Reel"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": true,
"enable_vl_resize": true,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"27",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"14": {
"inputs": {
"image": "7318418139276581_1759654853736_18 - コピー.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"19": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_niitk_00003_.png&type=temp&subfolder=&rand=0.9166876008508786"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_niitk_00004_.png&type=temp&subfolder=&rand=0.06689875639286158"
}
]
},
"image_a": [
"11",
1
],
"image_b": [
"8",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"20": {
"inputs": {
"filename_prefix": "qwenedit",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "请从图1中提取主要主体把背景设置为浅灰色并让主体正面朝向制作成产品照片。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"22": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"9",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"23": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"24": {
"inputs": {
"vae_name": "sdxl_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"25": {
"inputs": {
"samples": [
"23",
0
],
"vae": [
"24",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"26": {
"inputs": {
"images": [
"25",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"27": {
"inputs": {
"width": 720,
"height": 1280,
"upscale_method": "nearest-exact",
"keep_proportion": "pad",
"pad_color": "192,192,192",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"14",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"28": {
"inputs": {
"pixels": [
"27",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
}
}

View File

@ -0,0 +1,354 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 506786026379830,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"140",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"11",
6
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"84",
0
],
"image2": [
"82",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"15": {
"inputs": {
"image": "cloth_0026.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load cloth"
}
},
"21": {
"inputs": {
"value": "change clothes of image1 to image2, remove the cap from head"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"64": {
"inputs": {
"image": "Courtney_body.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load model"
}
},
"76": {
"inputs": {
"number": 832
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"77": {
"inputs": {
"number": 1248
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"78": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"82": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "crop",
"pad_color": "255,255,255",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"15",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"83": {
"inputs": {
"images": [
"82",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"84": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "crop",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"64",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"85": {
"inputs": {
"images": [
"84",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"93": {
"inputs": {
"filename_prefix": "qwenedit",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"132": {
"inputs": {
"images": [
"8",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"136": {
"inputs": {
"image": "281543721672978_1758880135639_0.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"137": {
"inputs": {
"detect_hand": "enable",
"detect_body": "enable",
"detect_face": "enable",
"resolution": 512,
"bbox_detector": "yolox_l.onnx",
"pose_estimator": "dw-ll_ucoco_384_bs5.torchscript.pt",
"scale_stick_for_xinsr_cn": "disable",
"image": [
"136",
0
]
},
"class_type": "DWPreprocessor",
"_meta": {
"title": "DWPose Estimator"
}
},
"139": {
"inputs": {
"images": [
"137",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"140": {
"inputs": {
"lora_name": "Try_On_Qwen_Edit_Lora.safetensors",
"strength_model": 1,
"model": [
"4",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
}
}

View File

@ -0,0 +1,111 @@
{
"1": {
"inputs": {
"image": "model_outfit_location_handbag3_1760086053609.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"2": {
"inputs": {
"enabled": true,
"swap_model": "inswapper_128.onnx",
"facedetection": "YOLOv5l",
"face_restore_model": "GPEN-BFR-1024.onnx",
"face_restore_visibility": 0.5200000000000001,
"codeformer_weight": 0.5,
"detect_gender_input": "no",
"detect_gender_source": "no",
"input_faces_index": "0",
"source_faces_index": "0",
"console_log_level": 1,
"input_image": [
"6",
0
],
"source_image": [
"3",
0
]
},
"class_type": "ReActorFaceSwap",
"_meta": {
"title": "ReActor 🌌 Fast Face Swap"
}
},
"3": {
"inputs": {
"image": "outfit_1760086053609.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"4": {
"inputs": {
"images": [
"2",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"6": {
"inputs": {
"resize_to": "4k",
"images": [
"1",
0
],
"upscaler_trt_model": [
"8",
0
]
},
"class_type": "UpscalerTensorrt",
"_meta": {
"title": "Upscaler Tensorrt ⚡"
}
},
"7": {
"inputs": {
"images": [
"6",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"8": {
"inputs": {
"model": "4x-UltraSharp",
"precision": "fp16"
},
"class_type": "LoadUpscalerTensorrtModel",
"_meta": {
"title": "Load Upscale Tensorrt Model"
}
},
"9": {
"inputs": {
"filename_prefix": "upscaled",
"images": [
"2",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}

View File

@ -71,7 +71,7 @@
}, },
"38": { "38": {
"inputs": { "inputs": {
"unet_name": "flux1-dev.safetensors", "unet_name": "flux1-krea-dev_fp8_scaled.safetensors",
"weight_dtype": "default" "weight_dtype": "default"
}, },
"class_type": "UNETLoader", "class_type": "UNETLoader",

View File

@ -0,0 +1,193 @@
{
"1": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp16.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"2": {
"inputs": {
"unet_name": "flux1-krea-dev_fp8_scaled.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"3": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"4": {
"inputs": {
"seed": 844515265883614,
"steps": 20,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"2",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"3",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"5": {
"inputs": {
"conditioning": [
"14",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"6": {
"inputs": {
"samples": [
"4",
0
],
"vae": [
"12",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"8": {
"inputs": {
"style_model_name": "flux1-redux-dev.safetensors"
},
"class_type": "StyleModelLoader",
"_meta": {
"title": "Load Style Model"
}
},
"9": {
"inputs": {
"crop": "center",
"clip_vision": [
"10",
0
],
"image": [
"13",
0
]
},
"class_type": "CLIPVisionEncode",
"_meta": {
"title": "CLIP Vision Encode"
}
},
"10": {
"inputs": {
"clip_name": "sigclip_vision_patch14_384.safetensors"
},
"class_type": "CLIPVisionLoader",
"_meta": {
"title": "Load CLIP Vision"
}
},
"11": {
"inputs": {
"strength": 0.6000000000000001,
"conditioning": [
"14",
0
],
"style_model": [
"8",
0
],
"clip_vision_output": [
"9",
0
]
},
"class_type": "ApplyStyleModelAdjust",
"_meta": {
"title": "Apply Style Model (Adjusted)"
}
},
"12": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"13": {
"inputs": {
"image": "7a103725df2576b79c7306f0d3050991.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image 1"
}
},
"14": {
"inputs": {
"text": "scify movie scene",
"clip": [
"1",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"15": {
"inputs": {
"filename_prefix": "STYLEDVIDEOMAKER",
"images": [
"6",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}

View File

@ -0,0 +1,229 @@
{
"1": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp16.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"2": {
"inputs": {
"unet_name": "flux1-krea-dev_fp8_scaled.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"3": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"4": {
"inputs": {
"seed": 445107772143446,
"steps": 20,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"2",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"3",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"5": {
"inputs": {
"conditioning": [
"14",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"6": {
"inputs": {
"samples": [
"4",
0
],
"vae": [
"12",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"8": {
"inputs": {
"style_model_name": "flux1-redux-dev.safetensors"
},
"class_type": "StyleModelLoader",
"_meta": {
"title": "Load Style Model"
}
},
"9": {
"inputs": {
"crop": "center",
"clip_vision": [
"10",
0
],
"image": [
"13",
0
]
},
"class_type": "CLIPVisionEncode",
"_meta": {
"title": "CLIP Vision Encode"
}
},
"10": {
"inputs": {
"clip_name": "sigclip_vision_patch14_384.safetensors"
},
"class_type": "CLIPVisionLoader",
"_meta": {
"title": "Load CLIP Vision"
}
},
"11": {
"inputs": {
"strength": 0.30000000000000004,
"conditioning": [
"14",
0
],
"style_model": [
"8",
0
],
"clip_vision_output": [
"9",
0
]
},
"class_type": "ApplyStyleModelAdjust",
"_meta": {
"title": "Apply Style Model (Adjusted)"
}
},
"12": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"13": {
"inputs": {
"image": "281543725739981_1759177922955_0.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image 1"
}
},
"14": {
"inputs": {
"text": "realistic photo of woman, wavy long blong hair, fullbody shot,, A dynamic dance scene begins with a distorted glitch effect mirroring the images grayscale aesthetic, quickly transitioning into a vibrant, fast-paced choreography featuring dancers in similar pale makeup and unsettling expressions. The music is electronic with heavy bass and industrial elements. The camera work should be kinetic and disorienting, utilizing quick cuts and unconventional angles, emphasizing the feeling of being trapped or haunted. The dance evolves from frantic movements to controlled yet eerie poses that echo the images gesture of covering the face. The setting changes between a stark white room similar to the image's background and abstract digital landscapes.",
"clip": [
"1",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"15": {
"inputs": {
"filename_prefix": "STYLEDVIDEOMAKER",
"images": [
"20",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"19": {
"inputs": {
"image": "face.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"20": {
"inputs": {
"enabled": true,
"swap_model": "inswapper_128.onnx",
"facedetection": "retinaface_resnet50",
"face_restore_model": "GPEN-BFR-2048.onnx",
"face_restore_visibility": 1,
"codeformer_weight": 1,
"detect_gender_input": "no",
"detect_gender_source": "no",
"input_faces_index": "0",
"source_faces_index": "0",
"console_log_level": 1,
"input_image": [
"6",
0
],
"source_image": [
"19",
0
]
},
"class_type": "ReActorFaceSwap",
"_meta": {
"title": "ReActor 🌌 Fast Face Swap"
}
}
}

View File

@ -0,0 +1,195 @@
{
"8": {
"inputs": {
"samples": [
"31",
0
],
"vae": [
"39",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "FACEIMAGE",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"27": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"31": {
"inputs": {
"seed": 161646847059712,
"steps": 20,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"45",
0
],
"positive": [
"41",
0
],
"negative": [
"42",
0
],
"latent_image": [
"27",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"38": {
"inputs": {
"unet_name": "flux1-krea-dev_fp8_scaled.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"39": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"40": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp16.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"41": {
"inputs": {
"clip_l": "realistic photo of 25 years old girl , face zoom up, Neutral face, long straight hair, pastel blue and purple hair color",
"t5xxl": "realistic photo of 25 years old girl , face zoom up, Neutral face, long straight hair, pastel blue and purple hair color",
"guidance": 3.5,
"clip": [
"40",
0
]
},
"class_type": "CLIPTextEncodeFlux",
"_meta": {
"title": "CLIPTextEncodeFlux"
}
},
"42": {
"inputs": {
"conditioning": [
"41",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"45": {
"inputs": {
"weight": 0.7700000000000001,
"start_at": 0,
"end_at": 1,
"model": [
"38",
0
],
"pulid_flux": [
"46",
0
],
"eva_clip": [
"47",
0
],
"face_analysis": [
"48",
0
],
"image": [
"49",
0
]
},
"class_type": "ApplyPulidFlux",
"_meta": {
"title": "Apply PuLID Flux"
}
},
"46": {
"inputs": {
"pulid_file": "pulid_flux_v0.9.1.safetensors"
},
"class_type": "PulidFluxModelLoader",
"_meta": {
"title": "Load PuLID Flux Model"
}
},
"47": {
"inputs": {},
"class_type": "PulidFluxEvaClipLoader",
"_meta": {
"title": "Load Eva Clip (PuLID Flux)"
}
},
"48": {
"inputs": {
"provider": "CUDA"
},
"class_type": "PulidFluxInsightFaceLoader",
"_meta": {
"title": "Load InsightFace (PuLID Flux)"
}
},
"49": {
"inputs": {
"image": "Generated Image September 12, 2025 - 1_04PM.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
}
}

View File

@ -97,7 +97,7 @@
}, },
"52": { "52": {
"inputs": { "inputs": {
"image": "ComfyUI_00036_.png" "image": "zagreb_musicspot_s14_c1_v1.png"
}, },
"class_type": "LoadImage", "class_type": "LoadImage",
"_meta": { "_meta": {
@ -133,7 +133,7 @@
"57": { "57": {
"inputs": { "inputs": {
"add_noise": "enable", "add_noise": "enable",
"noise_seed": 375574453154296, "noise_seed": 989367225141070,
"steps": 6, "steps": 6,
"cfg": 1, "cfg": 1,
"sampler_name": "euler", "sampler_name": "euler",
@ -216,9 +216,9 @@
}, },
"63": { "63": {
"inputs": { "inputs": {
"frame_rate": 32, "frame_rate": 24,
"loop_count": 0, "loop_count": 0,
"filename_prefix": "RADOMVIDEOMAKERVIDEO", "filename_prefix": "wan22_",
"format": "video/h264-mp4", "format": "video/h264-mp4",
"pix_fmt": "yuv420p", "pix_fmt": "yuv420p",
"crf": 19, "crf": 19,
@ -227,7 +227,7 @@
"pingpong": false, "pingpong": false,
"save_output": true, "save_output": true,
"images": [ "images": [
"71", "73",
0 0
] ]
}, },
@ -336,44 +336,11 @@
"title": "LoraLoaderModelOnly" "title": "LoraLoaderModelOnly"
} }
}, },
"71": {
"inputs": {
"ckpt_name": "rife49.pth",
"clear_cache_after_n_frames": 10,
"multiplier": 2,
"fast_mode": true,
"ensemble": true,
"scale_factor": 1,
"frames": [
"73",
0
]
},
"class_type": "RIFE VFI",
"_meta": {
"title": "RIFE VFI (recommend rife47 and rife49)"
}
},
"72": {
"inputs": {
"upscale_model": "4x-UltraSharp.pth",
"mode": "rescale",
"rescale_factor": 2.0000000000000004,
"resize_width": 832,
"resampling_method": "lanczos",
"supersample": "true",
"rounding_modulus": 8
},
"class_type": "CR Upscale Image",
"_meta": {
"title": "🔍 CR Upscale Image"
}
},
"73": { "73": {
"inputs": { "inputs": {
"resize_to": "4k", "resize_to": "4k",
"images": [ "images": [
"8", "76",
0 0
], ],
"upscaler_trt_model": [ "upscaler_trt_model": [
@ -388,12 +355,24 @@
}, },
"75": { "75": {
"inputs": { "inputs": {
"model": "4xNomos2_otf_esrgan", "model": "4x-UltraSharp",
"precision": "fp16" "precision": "fp16"
}, },
"class_type": "LoadUpscalerTensorrtModel", "class_type": "LoadUpscalerTensorrtModel",
"_meta": { "_meta": {
"title": "Load Upscale Tensorrt Model" "title": "Load Upscale Tensorrt Model"
} }
},
"76": {
"inputs": {
"anything": [
"8",
0
]
},
"class_type": "easy cleanGpuUsed",
"_meta": {
"title": "Clean VRAM Used"
}
} }
} }

View File

@ -0,0 +1,349 @@
{
"6": {
"inputs": {
"text": "Create an 8-second animated loop featuring a young man sitting on a stone ledge overlooking a nighttime cityscape. The scene should begin with a slow zoom into the boys face as he gazes upwards at the starry sky. Throughout the video, have shooting stars streak across the sky some fast, some slower, creating a dynamic visual effect. Gentle wind blows his hair and clothing.",
"clip": [
"38",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Positive Prompt)"
}
},
"7": {
"inputs": {
"text": "色调艳丽过曝静态细节模糊不清字幕风格作品画作画面静止整体发灰最差质量低质量JPEG压缩残留丑陋的残缺的多余的手指画得不好的手部画得不好的脸部畸形的毁容的形态畸形的肢体手指融合静止不动的画面杂乱的背景三条腿背景人很多倒着走",
"clip": [
"38",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"58",
0
],
"vae": [
"39",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"38": {
"inputs": {
"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
"type": "wan",
"device": "cpu"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"39": {
"inputs": {
"vae_name": "wan_2.1_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"50": {
"inputs": {
"width": [
"64",
1
],
"height": [
"64",
2
],
"length": 121,
"batch_size": 1,
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"vae": [
"39",
0
],
"start_image": [
"64",
0
]
},
"class_type": "WanImageToVideo",
"_meta": {
"title": "WanImageToVideo"
}
},
"52": {
"inputs": {
"image": "ComfyUI_00036_.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"54": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"69",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"55": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"70",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"57": {
"inputs": {
"add_noise": "enable",
"noise_seed": 375574453154296,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 3,
"return_with_leftover_noise": "enable",
"model": [
"54",
0
],
"positive": [
"50",
0
],
"negative": [
"50",
1
],
"latent_image": [
"50",
2
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"58": {
"inputs": {
"add_noise": "disable",
"noise_seed": 0,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 3,
"end_at_step": 10000,
"return_with_leftover_noise": "disable",
"model": [
"55",
0
],
"positive": [
"50",
0
],
"negative": [
"50",
1
],
"latent_image": [
"57",
0
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"61": {
"inputs": {
"unet_name": "wan2.2_i2v_high_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"62": {
"inputs": {
"unet_name": "wan2.2_i2v_low_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"63": {
"inputs": {
"frame_rate": 25,
"loop_count": 0,
"filename_prefix": "wan22_",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"images": [
"8",
0
]
},
"class_type": "VHS_VideoCombine",
"_meta": {
"title": "Video Combine 🎥🅥🅗🅢"
}
},
"64": {
"inputs": {
"width": 720,
"height": 1280,
"upscale_method": "lanczos",
"keep_proportion": "crop",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 16,
"device": "cpu",
"image": [
"52",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"65": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"61",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"66": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"65",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"67": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"62",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"68": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"67",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"69": {
"inputs": {
"lora_name": "Wan21_I2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 3.0000000000000004,
"model": [
"66",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"70": {
"inputs": {
"lora_name": "Wan21_T2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 1.5000000000000002,
"model": [
"68",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"75": {
"inputs": {
"model": "4xNomos2_otf_esrgan",
"precision": "fp16"
},
"class_type": "LoadUpscalerTensorrtModel",
"_meta": {
"title": "Load Upscale Tensorrt Model"
}
}
}

View File

@ -0,0 +1,581 @@
{
"70": {
"inputs": {
"text": "色调艳丽过曝静态细节模糊不清字幕风格作品画作画面静止整体发灰最差质量低质量JPEG压缩残留丑陋的残缺的多余的手指画得不好的手部画得不好的脸部畸形的毁容的形态畸形的肢体手指融合静止不动的画面杂乱的背景三条腿背景人很多倒着走",
"clip": [
"82",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
},
"73": {
"inputs": {
"vae_name": "wan_2.1_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"76": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"77",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"77": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"83",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"78": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"84",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"79": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"78",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"80": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"89",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"81": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"90",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"82": {
"inputs": {
"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
"type": "wan",
"device": "cpu"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"83": {
"inputs": {
"unet_name": "wan2.2_i2v_high_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"84": {
"inputs": {
"unet_name": "wan2.2_i2v_low_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"85": {
"inputs": {
"frame_rate": 32,
"loop_count": 0,
"filename_prefix": "STYLEDVIDEOMAKER",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"images": [
"88",
0
]
},
"class_type": "VHS_VideoCombine",
"_meta": {
"title": "Video Combine 🎥🅥🅗🅢"
}
},
"86": {
"inputs": {
"upscale_model": "4x-UltraSharp.pth",
"mode": "rescale",
"rescale_factor": 2.0000000000000004,
"resize_width": 832,
"resampling_method": "lanczos",
"supersample": "true",
"rounding_modulus": 8
},
"class_type": "CR Upscale Image",
"_meta": {
"title": "🔍 CR Upscale Image"
}
},
"87": {
"inputs": {
"samples": [
"92",
0
],
"vae": [
"73",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"88": {
"inputs": {
"ckpt_name": "rife49.pth",
"clear_cache_after_n_frames": 10,
"multiplier": 2,
"fast_mode": true,
"ensemble": true,
"scale_factor": 1,
"frames": [
"98",
0
]
},
"class_type": "RIFE VFI",
"_meta": {
"title": "RIFE VFI (recommend rife47 and rife49)"
}
},
"89": {
"inputs": {
"lora_name": "Wan21_I2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 3.0000000000000004,
"model": [
"76",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"90": {
"inputs": {
"lora_name": "Wan21_T2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 1.5000000000000002,
"model": [
"79",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"91": {
"inputs": {
"add_noise": "enable",
"noise_seed": 452107028428,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 3,
"return_with_leftover_noise": "enable",
"model": [
"80",
0
],
"positive": [
"96",
0
],
"negative": [
"96",
1
],
"latent_image": [
"96",
2
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"92": {
"inputs": {
"add_noise": "disable",
"noise_seed": 0,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 3,
"end_at_step": 10000,
"return_with_leftover_noise": "disable",
"model": [
"81",
0
],
"positive": [
"96",
0
],
"negative": [
"96",
1
],
"latent_image": [
"91",
0
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"93": {
"inputs": {
"model": "4xNomos2_otf_esrgan",
"precision": "fp16"
},
"class_type": "LoadUpscalerTensorrtModel",
"_meta": {
"title": "Load Upscale Tensorrt Model"
}
},
"95": {
"inputs": {
"text": "A luminous ballerina in mid-performance, illustrated in glowing white strokes against a deep black background. She balances gracefully en pointe on one foot, arms extended in a fluid pose, her tutu radiating light and motion like ethereal fabric made of starlight. Her hair flows upward, sketched in swirling white lines, blending with scattered glowing stars around her. The reflection of her figure shimmers on a dark water surface below, surrounded by circular ripples of light. The style is dreamy, abstract, and expressive, combining sketch-like brush strokes with glowing energy lines. High contrast, elegant, surreal ballet illustration.",
"clip": [
"82",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Positive Prompt)"
}
},
"96": {
"inputs": {
"width": [
"97",
1
],
"height": [
"97",
2
],
"length": 121,
"batch_size": 1,
"positive": [
"95",
0
],
"negative": [
"70",
0
],
"vae": [
"73",
0
],
"start_image": [
"97",
0
]
},
"class_type": "WanImageToVideo",
"_meta": {
"title": "WanImageToVideo"
}
},
"97": {
"inputs": {
"width": 320,
"height": 640,
"upscale_method": "lanczos",
"keep_proportion": "crop",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 16,
"device": "cpu",
"image": [
"103",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"98": {
"inputs": {
"resize_to": "4k",
"images": [
"87",
0
],
"upscaler_trt_model": [
"93",
0
]
},
"class_type": "UpscalerTensorrt",
"_meta": {
"title": "Upscaler Tensorrt ⚡"
}
},
"99": {
"inputs": {
"filename_prefix": "STYLEDVIDEOMAKER",
"images": [
"103",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"100": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp16.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"101": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"102": {
"inputs": {
"conditioning": [
"105",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"103": {
"inputs": {
"samples": [
"106",
0
],
"vae": [
"101",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"104": {
"inputs": {
"width": 320,
"height": 640,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"105": {
"inputs": {
"text": "A luminous ballerina in mid-performance, illustrated in glowing white strokes against a deep black background. She balances gracefully en pointe on one foot, arms extended in a fluid pose, her tutu radiating light and motion like ethereal fabric made of starlight. Her hair flows upward, sketched in swirling white lines, blending with scattered glowing stars around her. The reflection of her figure shimmers on a dark water surface below, surrounded by circular ripples of light. The style is dreamy, abstract, and expressive, combining sketch-like brush strokes with glowing energy lines. High contrast, elegant, surreal ballet illustration.",
"clip": [
"100",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"106": {
"inputs": {
"seed": 84283616550942,
"steps": 20,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"107",
0
],
"positive": [
"111",
0
],
"negative": [
"102",
0
],
"latent_image": [
"104",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"107": {
"inputs": {
"unet_name": "flux1-krea-dev_fp8_scaled.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"110": {
"inputs": {
"style_model_name": "flux1-redux-dev.safetensors"
},
"class_type": "StyleModelLoader",
"_meta": {
"title": "Load Style Model"
}
},
"111": {
"inputs": {
"strength": 0.5,
"conditioning": [
"105",
0
],
"style_model": [
"110",
0
],
"clip_vision_output": [
"112",
0
]
},
"class_type": "ApplyStyleModelAdjust",
"_meta": {
"title": "Apply Style Model (Adjusted)"
}
},
"112": {
"inputs": {
"crop": "center",
"clip_vision": [
"113",
0
],
"image": [
"114",
0
]
},
"class_type": "CLIPVisionEncode",
"_meta": {
"title": "CLIP Vision Encode"
}
},
"113": {
"inputs": {
"clip_name": "sigclip_vision_patch14_384.safetensors"
},
"class_type": "CLIPVisionLoader",
"_meta": {
"title": "Load CLIP Vision"
}
},
"114": {
"inputs": {
"image": "440fb3cb2bcc993bdc7da34986a7135d.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
}
}

View File

@ -0,0 +1,545 @@
{
"70": {
"inputs": {
"text": "色调艳丽过曝静态细节模糊不清字幕风格作品画作画面静止整体发灰最差质量低质量JPEG压缩残留丑陋的残缺的多余的手指画得不好的手部画得不好的脸部畸形的毁容的形态畸形的肢体手指融合静止不动的画面杂乱的背景三条腿背景人很多倒着走",
"clip": [
"82",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
},
"73": {
"inputs": {
"vae_name": "wan_2.1_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"76": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"77",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"77": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"83",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"78": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"84",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"79": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"78",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"80": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"89",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"81": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"90",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"82": {
"inputs": {
"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
"type": "wan",
"device": "cpu"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"83": {
"inputs": {
"unet_name": "wan2.2_i2v_high_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"84": {
"inputs": {
"unet_name": "wan2.2_i2v_low_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"85": {
"inputs": {
"frame_rate": 32,
"loop_count": 0,
"filename_prefix": "STYLEDVIDEOMAKER",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"images": [
"87",
0
]
},
"class_type": "VHS_VideoCombine",
"_meta": {
"title": "Video Combine 🎥🅥🅗🅢"
}
},
"86": {
"inputs": {
"upscale_model": "4x-UltraSharp.pth",
"mode": "rescale",
"rescale_factor": 2.0000000000000004,
"resize_width": 832,
"resampling_method": "lanczos",
"supersample": "true",
"rounding_modulus": 8
},
"class_type": "CR Upscale Image",
"_meta": {
"title": "🔍 CR Upscale Image"
}
},
"87": {
"inputs": {
"samples": [
"92",
0
],
"vae": [
"73",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"89": {
"inputs": {
"lora_name": "Wan21_I2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 3.0000000000000004,
"model": [
"76",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"90": {
"inputs": {
"lora_name": "Wan21_T2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 1.5000000000000002,
"model": [
"79",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"91": {
"inputs": {
"add_noise": "enable",
"noise_seed": 452107028428,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 3,
"return_with_leftover_noise": "enable",
"model": [
"80",
0
],
"positive": [
"96",
0
],
"negative": [
"96",
1
],
"latent_image": [
"96",
2
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"92": {
"inputs": {
"add_noise": "disable",
"noise_seed": 0,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 3,
"end_at_step": 10000,
"return_with_leftover_noise": "disable",
"model": [
"81",
0
],
"positive": [
"96",
0
],
"negative": [
"96",
1
],
"latent_image": [
"91",
0
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"94": {
"inputs": {
"image": "ComfyUI_00036_.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"95": {
"inputs": {
"text": "A luminous ballerina in mid-performance, illustrated in glowing white strokes against a deep black background. She balances gracefully en pointe on one foot, arms extended in a fluid pose, her tutu radiating light and motion like ethereal fabric made of starlight. Her hair flows upward, sketched in swirling white lines, blending with scattered glowing stars around her. The reflection of her figure shimmers on a dark water surface below, surrounded by circular ripples of light. The style is dreamy, abstract, and expressive, combining sketch-like brush strokes with glowing energy lines. High contrast, elegant, surreal ballet illustration.",
"clip": [
"82",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Positive Prompt)"
}
},
"96": {
"inputs": {
"width": [
"97",
1
],
"height": [
"97",
2
],
"length": 89,
"batch_size": 1,
"positive": [
"95",
0
],
"negative": [
"70",
0
],
"vae": [
"73",
0
],
"start_image": [
"97",
0
]
},
"class_type": "WanImageToVideo",
"_meta": {
"title": "WanImageToVideo"
}
},
"97": {
"inputs": {
"width": 320,
"height": 640,
"upscale_method": "lanczos",
"keep_proportion": "crop",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 16,
"device": "cpu",
"image": [
"103",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"99": {
"inputs": {
"filename_prefix": "STYLEDVIDEOMAKER",
"images": [
"103",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"100": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp16.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"101": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"102": {
"inputs": {
"conditioning": [
"105",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"103": {
"inputs": {
"samples": [
"106",
0
],
"vae": [
"101",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"104": {
"inputs": {
"width": 320,
"height": 640,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"105": {
"inputs": {
"text": "A luminous ballerina in mid-performance, illustrated in glowing white strokes against a deep black background. She balances gracefully en pointe on one foot, arms extended in a fluid pose, her tutu radiating light and motion like ethereal fabric made of starlight. Her hair flows upward, sketched in swirling white lines, blending with scattered glowing stars around her. The reflection of her figure shimmers on a dark water surface below, surrounded by circular ripples of light. The style is dreamy, abstract, and expressive, combining sketch-like brush strokes with glowing energy lines. High contrast, elegant, surreal ballet illustration.",
"clip": [
"100",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"106": {
"inputs": {
"seed": 84283616550942,
"steps": 20,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"107",
0
],
"positive": [
"111",
0
],
"negative": [
"102",
0
],
"latent_image": [
"104",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"107": {
"inputs": {
"unet_name": "flux1-krea-dev_fp8_scaled.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"110": {
"inputs": {
"style_model_name": "flux1-redux-dev.safetensors"
},
"class_type": "StyleModelLoader",
"_meta": {
"title": "Load Style Model"
}
},
"111": {
"inputs": {
"strength": 0.15,
"conditioning": [
"105",
0
],
"style_model": [
"110",
0
],
"clip_vision_output": [
"112",
0
]
},
"class_type": "ApplyStyleModelAdjust",
"_meta": {
"title": "Apply Style Model (Adjusted)"
}
},
"112": {
"inputs": {
"crop": "center",
"clip_vision": [
"113",
0
],
"image": [
"114",
0
]
},
"class_type": "CLIPVisionEncode",
"_meta": {
"title": "CLIP Vision Encode"
}
},
"113": {
"inputs": {
"clip_name": "sigclip_vision_patch14_384.safetensors"
},
"class_type": "CLIPVisionLoader",
"_meta": {
"title": "Load CLIP Vision"
}
},
"114": {
"inputs": {
"image": "440fb3cb2bcc993bdc7da34986a7135d.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
}
}

View File

@ -0,0 +1,388 @@
{
"4": {
"inputs": {
"ckpt_name": "dreamshaperXL_v21TurboDPMSDE.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"12": {
"inputs": {
"seed": 302411063911982,
"steps": 8,
"cfg": 2,
"sampler_name": "dpmpp_sde",
"scheduler": "karras",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"65",
0
],
"negative": [
"69",
0
],
"latent_image": [
"13",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"13": {
"inputs": {
"width": 1216,
"height": 832,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"16": {
"inputs": {
"samples": [
"12",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"36": {
"inputs": {
"method": "Mixture of Diffusers",
"tile_width": 1024,
"tile_height": 1024,
"tile_overlap": 32,
"tile_batch_size": 8,
"model": [
"4",
0
]
},
"class_type": "TiledDiffusion",
"_meta": {
"title": "Tiled Diffusion"
}
},
"51": {
"inputs": {
"tile_size": 1024,
"fast": false,
"samples": [
"80",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecodeTiled_TiledDiffusion",
"_meta": {
"title": "Tiled VAE Decode"
}
},
"65": {
"inputs": {
"text": "photo of a high end sports car",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"69": {
"inputs": {
"text": "text, watermark, (film grain, noise:1.2)",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"80": {
"inputs": {
"seed": 105566927616764,
"steps": 4,
"cfg": 2,
"sampler_name": "dpmpp_sde",
"scheduler": "karras",
"denoise": 1,
"model": [
"36",
0
],
"positive": [
"141",
0
],
"negative": [
"141",
1
],
"latent_image": [
"84",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"84": {
"inputs": {
"width": [
"106",
0
],
"height": [
"107",
0
],
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"105": {
"inputs": {
"image": [
"115",
0
]
},
"class_type": "GetImageSizeAndCount",
"_meta": {
"title": "Get Image Size & Count"
}
},
"106": {
"inputs": {
"value": "a*b",
"a": [
"105",
1
],
"b": [
"117",
0
]
},
"class_type": "SimpleMath+",
"_meta": {
"title": "🔧 Simple Math"
}
},
"107": {
"inputs": {
"value": "a*b",
"a": [
"105",
2
],
"b": [
"117",
0
]
},
"class_type": "SimpleMath+",
"_meta": {
"title": "🔧 Simple Math"
}
},
"111": {
"inputs": {
"image": "model_outfit_location_handbag1_1760092227085.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"115": {
"inputs": {
"any_01": [
"111",
0
]
},
"class_type": "Any Switch (rgthree)",
"_meta": {
"title": "Any Switch (rgthree)"
}
},
"117": {
"inputs": {
"value": 4.000000000000001
},
"class_type": "FloatConstant",
"_meta": {
"title": "Float Constant"
}
},
"133": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_ybqmm_00009_.png&type=temp&subfolder=&rand=0.02707950499627365"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_ybqmm_00010_.png&type=temp&subfolder=&rand=0.18690183070180255"
}
]
},
"image_a": [
"115",
0
],
"image_b": [
"149",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"141": {
"inputs": {
"strength": 0.65,
"start_percent": 0,
"end_percent": 0.9,
"positive": [
"65",
0
],
"negative": [
"69",
0
],
"control_net": [
"142",
0
],
"image": [
"115",
0
]
},
"class_type": "ACN_AdvancedControlNetApply",
"_meta": {
"title": "Apply Advanced ControlNet 🛂🅐🅒🅝"
}
},
"142": {
"inputs": {
"control_net_name": "xinsircontrolnet-tile-sdxl-1.0.safetensors"
},
"class_type": "ControlNetLoaderAdvanced",
"_meta": {
"title": "Load Advanced ControlNet Model 🛂🅐🅒🅝"
}
},
"148": {
"inputs": {
"color_space": "LAB",
"factor": 0.8,
"device": "auto",
"batch_size": 0,
"image": [
"51",
0
],
"reference": [
"115",
0
]
},
"class_type": "ImageColorMatch+",
"_meta": {
"title": "🔧 Image Color Match"
}
},
"149": {
"inputs": {
"sharpen_radius": 1,
"sigma": 1,
"alpha": 0.05,
"image": [
"148",
0
]
},
"class_type": "ImageSharpen",
"_meta": {
"title": "Image Sharpen"
}
},
"154": {
"inputs": {
"filename_prefix": "Upscaled",
"images": [
"149",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"165": {
"inputs": {
"image": "model_outfit_location_handbag1_1760092227085.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"166": {
"inputs": {
"filename_prefix": "upscaled",
"images": [
"149",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}

View File

@ -0,0 +1,425 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 559577834683401,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"66",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"11",
6
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"84",
0
],
"image2": [
"82",
0
],
"image3": [
"81",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"15": {
"inputs": {
"image": "Allison_body (1).png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"20": {
"inputs": {
"filename_prefix": "qwenedit",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "图2中的女孩穿着图1的衣服并以图3的姿势站立。背景保持浅灰色。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"64": {
"inputs": {
"image": "cloth_0111.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"66": {
"inputs": {
"lora_name": "extract-outfit_v3.safetensors",
"strength_model": 1,
"model": [
"4",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"67": {
"inputs": {
"detect_hand": "enable",
"detect_body": "enable",
"detect_face": "enable",
"resolution": 512,
"bbox_detector": "yolox_l.onnx",
"pose_estimator": "dw-ll_ucoco_384_bs5.torchscript.pt",
"scale_stick_for_xinsr_cn": "disable",
"image": [
"68",
0
]
},
"class_type": "DWPreprocessor",
"_meta": {
"title": "DWPose Estimator"
}
},
"68": {
"inputs": {
"image": "633387441703331_1758877367350_1.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"69": {
"inputs": {
"images": [
"81",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"76": {
"inputs": {
"number": 720
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"77": {
"inputs": {
"number": 1280
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"78": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"81": {
"inputs": {
"width": 480,
"height": 962,
"upscale_method": "nearest-exact",
"keep_proportion": "pad",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"67",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"82": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "crop",
"pad_color": "255,255,255",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"15",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"83": {
"inputs": {
"images": [
"82",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"84": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "pad",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"64",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"85": {
"inputs": {
"images": [
"84",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"86": {
"inputs": {
"image1_text": "image1",
"image2_text": "image2",
"image3_text": "image3",
"image4_text": "image4",
"reel_height": 512,
"border": 32,
"image1": [
"15",
0
],
"image2": [
"64",
0
],
"image3": [
"81",
0
],
"image4": [
"8",
0
]
},
"class_type": "LayerUtility: ImageReel",
"_meta": {
"title": "LayerUtility: Image Reel"
}
},
"87": {
"inputs": {
"font_file": "Alibaba-PuHuiTi-Heavy.ttf",
"font_size": 40,
"border": 32,
"color_theme": "light",
"reel_1": [
"86",
0
]
},
"class_type": "LayerUtility: ImageReelComposit",
"_meta": {
"title": "LayerUtility: Image Reel Composit"
}
},
"88": {
"inputs": {
"filename_prefix": "vtonresult/vton",
"images": [
"87",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}

View File

@ -0,0 +1,357 @@
{
"1": {
"inputs": {
"unet_name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"2": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"3": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"4": {
"inputs": {
"lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"strength_model": 1,
"model": [
"1",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"5": {
"inputs": {
"conditioning": [
"11",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"7": {
"inputs": {
"seed": 559577834683401,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"66",
0
],
"positive": [
"11",
0
],
"negative": [
"5",
0
],
"latent_image": [
"11",
6
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"8": {
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"11": {
"inputs": {
"prompt": [
"21",
0
],
"enable_resize": false,
"enable_vl_resize": false,
"upscale_method": "lanczos",
"crop": "disabled",
"instruction": "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
"clip": [
"2",
0
],
"vae": [
"3",
0
],
"image1": [
"84",
0
],
"image2": [
"82",
0
]
},
"class_type": "TextEncodeQwenImageEditPlus_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlus 小志Jason(xiaozhijason)"
}
},
"15": {
"inputs": {
"image": "Allison_body (1).png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"20": {
"inputs": {
"filename_prefix": "qwenedit",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"21": {
"inputs": {
"value": "图2中的人物穿着图1的上衣、下装和配饰。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "String (Multiline)"
}
},
"64": {
"inputs": {
"image": "cloth_0111.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"66": {
"inputs": {
"lora_name": "extract-outfit_v3.safetensors",
"strength_model": 1,
"model": [
"4",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"76": {
"inputs": {
"number": 720
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"77": {
"inputs": {
"number": 1280
},
"class_type": "StaticNumberInt",
"_meta": {
"title": "Static Number Int"
}
},
"78": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"82": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "crop",
"pad_color": "255,255,255",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"15",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"83": {
"inputs": {
"images": [
"82",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"84": {
"inputs": {
"width": [
"76",
0
],
"height": [
"77",
0
],
"upscale_method": "nearest-exact",
"keep_proportion": "pad",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"64",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"85": {
"inputs": {
"images": [
"84",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
},
"86": {
"inputs": {
"image1_text": "image1",
"image2_text": "image2",
"image3_text": "image3",
"image4_text": "image4",
"reel_height": 512,
"border": 32,
"image1": [
"15",
0
],
"image2": [
"64",
0
],
"image3": [
"8",
0
]
},
"class_type": "LayerUtility: ImageReel",
"_meta": {
"title": "LayerUtility: Image Reel"
}
},
"87": {
"inputs": {
"font_file": "Alibaba-PuHuiTi-Heavy.ttf",
"font_size": 40,
"border": 32,
"color_theme": "light",
"reel_1": [
"86",
0
]
},
"class_type": "LayerUtility: ImageReelComposit",
"_meta": {
"title": "LayerUtility: Image Reel Composit"
}
},
"88": {
"inputs": {
"filename_prefix": "vtonresult/vton",
"images": [
"87",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}

View File

@ -18,13 +18,11 @@ const servers = [
baseUrl: process.env.SERVER1_COMFY_BASE_URL, baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR, outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
}, },
/*
{ {
baseUrl: process.env.SERVER2_COMFY_BASE_URL, baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR, outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
}, },
*/
]; ];
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@ -63,7 +61,7 @@ async function worker(server: any) {
while (true) { while (true) {
await sleep(Math.random() * 3000); // Random delay await sleep(Math.random() * 3000); // Random delay
const videosToProcess = (await query( const videosToProcess = (await query(
"SELECT * FROM video WHERE image_prompt IS NOT NULL AND (image_path IS NULL OR image_path = '') LIMIT 1" "SELECT * FROM video WHERE (image_path IS NULL OR image_path = '') LIMIT 1"
)) as any[]; )) as any[];
if (videosToProcess.length === 0) { if (videosToProcess.length === 0) {

View File

@ -1,167 +0,0 @@
import fs from 'fs';
import path from 'path';
import { query } from './lib/mysql';
import { logger } from './lib/logger';
import { callLMStudio } from './lib/lmstudio';
async function main() {
await updatePromptsFromDB();
process.exit();
}
/**
* Find DB records whose video_prompt contains 'cut' or 'zoom' (case-insensitive),
* regenerate the video_prompt using LMStudio, and update the record.
*
* If the newly generated prompt still contains any banned words/phrases, regenerate
* again (up to maxAttempts). If after attempts the prompt is still invalid, skip update.
*/
async function updatePromptsFromDB() {
logger.info("Starting DB sweep for video_prompt containing 'cut' or 'zoom'...");
// Banned regex per requirement
const banned = /\b(cut|cuts|cutting|quick cut|insert|macro insert|close-?up|extreme close-?up|zoom|zooming|push-?in|pull-?out|whip|switch angle|change angle|montage|cross-?cut|smash cut|transition|meanwhile|later)\b/i;
let rows: any[] = [];
try {
// Case-insensitive search for 'cut' or 'zoom' anywhere in video_prompt
rows = (await query(
"SELECT id, genre, sub_genre, scene, action, camera, video_prompt FROM video WHERE LOWER(COALESCE(video_prompt,'')) LIKE ? OR LOWER(COALESCE(video_prompt,'')) LIKE ?",
['%cut%', '%zoom%']
)) as any[];
} catch (err) {
logger.error('DB query failed while searching for problematic prompts:', err);
return;
}
if (!rows || rows.length === 0) {
logger.info("No records found with 'cut' or 'zoom' in video_prompt.");
return;
}
logger.info(`Found ${rows.length} record(s) to process.`);
for (const row of rows) {
const id = row.id;
const genre = row.genre || '';
const subGenre = row.sub_genre || '';
const scene = row.scene || '';
const action = row.action || '';
const camera = row.camera || '';
if (!genre || !subGenre || !scene) {
logger.info(`Skipping id=${id} due to missing identification fields: genre='${genre}', sub_genre='${subGenre}', scene='${scene}'`);
continue;
}
// Build LM input (similar ruleset to previous implementation)
const lmInput = buildLMInputFromRecord(genre, subGenre, scene, action, camera, row.video_prompt);
let finalPrompt: string | null = null;
const maxAttempts = 10;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let lmResponse: any = null;
try {
lmResponse = await callLMStudio(lmInput);
} catch (err) {
logger.warn(`LMStudio call failed for id=${id} (attempt ${attempt}): ${err}`);
// Retry on next loop iteration
continue;
}
if (!lmResponse) {
logger.warn(`LMStudio returned empty response for id=${id} (attempt ${attempt}).`);
continue;
}
const videoPrompt = lmResponse.videoPrompt || lmResponse.video_prompt || lmResponse.prompt || null;
if (!videoPrompt || typeof videoPrompt !== 'string') {
logger.warn(`LMStudio did not return a valid videoPrompt for id=${id} (attempt ${attempt}).`);
continue;
}
// Check banned regex
if (banned.test(videoPrompt)) {
logger.info(`Generated prompt for id=${id} (attempt ${attempt}) still contains banned phrases - retrying.`);
logger.info(videoPrompt);
// If last attempt, we will fall through and skip update
continue;
}
// Passed banned check
finalPrompt = videoPrompt;
break;
}
if (!finalPrompt) {
logger.warn(`Could not generate a clean prompt for id=${id} after ${maxAttempts} attempts. Skipping update.`);
continue;
}
// Update DB
try {
await query('UPDATE video SET video_prompt = ? WHERE id = ?', [finalPrompt, id]);
logger.info(`Updated video_prompt for id=${id}`);
} catch (err) {
logger.error(`Failed to update video_prompt for id=${id}: ${err}`);
}
}
logger.info('Finished DB sweep for problematic prompts.');
}
/**
* Helper to construct LM input for a single DB record.
* Keeps the same HARD RULES and prohibited list as previous data-driven generation.
*/
function buildLMInputFromRecord(
genre: string,
subGenre: string,
finalScene: string,
chosenAction: string,
camera: string,
existingPrompt: string | undefined
) {
const accents = 'none';
const mood = 'n/a';
const lighting = 'n/a';
const style = 'n/a';
const lmInput = `
Return exactly one JSON object: { "videoPrompt": "..." } and nothing else.
Write "videoPrompt" in 100150 words, present tense, plain concrete language.
HARD RULES (must comply):
- One continuous shot ("one take", "oner"). Real-time 8 seconds. No edits.
- Fixed location and vantage. Do not change background or angle.
- Lens and focal length locked. No zooms, no close-ups that imply a lens change, no rack zoom.
- Camera motion: at most subtle pan/tilt/dolly within 1 meter while staying in the same spot.
- Keep framing consistent (e.g., medium-wide two-shot). No “another shot/meanwhile.”
- Describe: (1) main action, (2) framing & motion, (3) lighting & mood, (4) style & small accents.
- Use clear simple sentences. No metaphors or poetic language.
PROHIBITED WORDS/PHRASES (case-insensitive):
cut, cuts, cutting, quick cut, insert, macro insert, close-up, extreme close-up,
zoom, zooms, zooming, push-in, pull-out, whip, switch angle, change angle,
montage, cross-cut, smash cut, transition, meanwhile, later.
If proximity is needed, say: "the camera glides slightly closer while staying in the same position."
Here is information of the scene, please generate prompt for the video based on these information for key "videoPrompt":
Genre: ${genre}
Sub-Genre: ${subGenre}
Scene: ${finalScene}
Action: ${chosenAction || 'n/a'}
Camera: ${camera || 'static or subtle movement (stay within scene)'}
Accents: ${accents}
Mood: ${mood}
Lighting: ${lighting}
Style: ${style}
`;
return lmInput;
}
main();

View File

@ -0,0 +1,300 @@
import fs from 'fs';
import path from 'path';
import { query } from './lib/mysql';
import { logger } from './lib/logger';
import { callLMStudioWithFile } from './lib/lmstudio';
async function main() {
await updatePromptsFromDB();
process.exit();
}
/**
* Utility: extract JSON substring from a text.
* Tries to extract from fenced ```json blocks first, otherwise extracts first {...} span.
*/
function extractJsonFromText(text: string): any | null {
if (!text || typeof text !== 'string') return null;
// Try fenced code block with optional json language
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (fenced && fenced[1]) {
try {
return JSON.parse(fenced[1].trim());
} catch (e) {
// fall through to brace extraction
}
}
// Try to extract first {...} match (greedy between first { and last })
const brace = text.match(/\{[\s\S]*\}/);
if (brace && brace[0]) {
try {
return JSON.parse(brace[0]);
} catch (e) {
return null;
}
}
return null;
}
/**
* Wrapper to call LMStudio with an image and prompt, and extract JSON reliably.
* - Uses callLMStudioWithFile to pass the image.
* - Tries to parse JSON from response if needed.
* - Retries up to maxRetries times (default 5) when parsing fails or an error occurs.
*/
async function callLMWithImageAndExtract(imagePath: string, prompt: string, maxRetries = 5): Promise<any | null> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await callLMStudioWithFile(imagePath, prompt);
// callLMStudioWithFile attempts to return parsed JSON already. Accept objects directly.
if (res && typeof res === 'object') {
return res;
}
// If it returned text, try to extract JSON
if (typeof res === 'string') {
const parsed = extractJsonFromText(res);
if (parsed) return parsed;
}
logger.warn(`callLMWithImageAndExtract: attempt ${attempt} returned unexpected result. Retrying...`);
} catch (err) {
logger.warn(`callLMWithImageAndExtract: attempt ${attempt} failed: ${err}`);
}
}
logger.error(`callLMWithImageAndExtract: failed to get valid JSON after ${maxRetries} attempts`);
return null;
}
/**
* Main sweep: find DB records whose video_prompt contains 'cut' or 'zoom' (case-insensitive),
* run multi-step LMStudio flow (object -> action -> camerawork -> final prompt) using the image,
* and update the record.
*/
async function updatePromptsFromDB() {
logger.info("Starting DB sweep for video_prompt containing 'cut' or 'zoom'...");
// Banned regex per requirement
const banned = /\b(cut|cuts|cutting|quick cut|insert|macro insert|close-?up|extreme close-?up|zoom|zooming|push-?in|pull-?out|whip|switch angle|change angle|montage|cross-?cut|smash cut|transition|meanwhile|later)\b/i;
let rows: any[] = [];
try {
// Case-insensitive search for 'cut' or 'zoom' anywhere in video_prompt
rows = (await query(
"SELECT id, genre, sub_genre, scene, action, camera, video_prompt, image_path FROM video where (video_path = '' or video_path is null) and modified_at < '2025-08-30 09:15:33'",
)) as any[];
} catch (err) {
logger.error('DB query failed while searching for problematic prompts:', err);
return;
}
if (!rows || rows.length === 0) {
logger.info("No records found with 'cut' or 'zoom' in video_prompt.");
return;
}
logger.info(`Found ${rows.length} record(s) to process.`);
for (const row of rows) {
const id = row.id;
const genre = row.genre || '';
const subGenre = row.sub_genre || '';
const scene = row.scene || '';
const action = row.action || '';
const camera = row.camera || '';
const imagePathRaw = row.image_path || row.image || null;
if (!genre || !subGenre || !scene) {
logger.info(`Skipping id=${id} due to missing identification fields: genre='${genre}', sub_genre='${subGenre}', scene='${scene}'`);
continue;
}
if (!imagePathRaw) {
logger.info(`Skipping id=${id} because image_path is empty for this record.`);
continue;
}
// Resolve the image path: if relative, make absolute based on cwd
let imageFullPath = imagePathRaw;
if (!path.isAbsolute(imageFullPath)) {
imageFullPath = path.resolve(process.cwd(), imageFullPath);
}
if (!fs.existsSync(imageFullPath)) {
logger.info(`Skipping id=${id} because image not found at path: ${imageFullPath}`);
continue;
}
logger.info(`Processing id=${id} using image: ${imageFullPath}`);
// Step 1: Detect main object
const step1Prompt = `
Return exactly one JSON object and nothing else: { "mainobject": "..." }.
Look at the provided image and determine the single most prominent/main object or subject in the scene.
Answer with a short noun or short phrase (no extra commentary).
If unsure, give the best concise guess.
`;
const step1Res = await callLMWithImageAndExtract(imageFullPath, step1Prompt, 5);
const mainobject = (step1Res && (step1Res.mainobject || step1Res.mainObject || step1Res.object)) ? String(step1Res.mainobject || step1Res.mainObject || step1Res.object).trim() : '';
if (!mainobject) {
logger.warn(`id=${id} - could not detect main object. Skipping record.`);
continue;
}
logger.info(`id=${id} - detected main object: ${mainobject}`);
// Step 2: Determine best action for this scene
const step2Prompt = `
You have access to the image and the detected main object: "${mainobject}".
Decide which single action type best fits this scene from the list:
- no action
- micro animation (animate object but small movement)
- big movement
- impossible movement
Return exactly one JSON object and nothing else: { "actiontype": "...", "action": ""}.
Do not add commentary. Choose the single best option from the list above.
`;
const step2Res = await callLMWithImageAndExtract(imageFullPath, step2Prompt, 5);
const actiontype = (step2Res && (step2Res.actiontype || step2Res.actionType)) ? String(step2Res.actiontype || step2Res.actionType).trim() : '';
if (!actiontype) {
logger.warn(`id=${id} - could not determine action type. Skipping record.`);
continue;
}
logger.info(`id=${id} - decided action type: ${actiontype}`);
// Step 3: Ask LMStudio what is the best camera work for the scene
const step3Prompt = `
Given the image and the following information:
- main object: "${mainobject}"
- chosen action type: "${actiontype}"
From the options below pick the single best camera approach for this scene:
- static camera
- pan
- rotation
- follow the moving object
- zoom to the object
- impossible camera work
Return exactly one JSON object and nothing else: { "cameraworkType": "..." }.
Choose one of the listed options and do not add commentary.
`;
const step3Res = await callLMWithImageAndExtract(imageFullPath, step3Prompt, 5);
const cameraworkType = (step3Res && (step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera)) ? String(step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera).trim() : '';
if (!cameraworkType) {
logger.warn(`id=${id} - could not determine camera work. Skipping record.`);
continue;
}
logger.info(`id=${id} - decided camera work: ${cameraworkType}`);
// Step 4: Generate final video prompt using all gathered info
const finalPromptInput = buildLMInputFromRecordWithImageInfo(
genre,
subGenre,
scene,
action,
camera,
mainobject,
actiontype,
cameraworkType
);
// Use wrapper to call LM and extract JSON { videoPrompt: "" }
const finalRes = await callLMWithImageAndExtract(imageFullPath, finalPromptInput, 5);
const videoPrompt = (finalRes && (finalRes.videoPrompt || finalRes.video_prompt || finalRes.prompt)) ? String(finalRes.videoPrompt || finalRes.video_prompt || finalRes.prompt).trim() : null;
logger.info(`id=${id} - videoPrompt: ${videoPrompt}`);
if (!videoPrompt) {
logger.warn(`id=${id} - LM did not return a valid videoPrompt. Skipping record.`);
continue;
}
// Check banned regex
if (banned.test(videoPrompt)) {
logger.info(`Generated prompt for id=${id} contains banned phrases - skipping update.`);
logger.info(videoPrompt);
continue;
}
// Update DB
try {
await query('UPDATE video SET video_prompt = ? WHERE id = ?', [videoPrompt, id]);
logger.info(`Updated video_prompt for id=${id}`);
} catch (err) {
logger.error(`Failed to update video_prompt for id=${id}: ${err}`);
}
}
logger.info('Finished DB sweep for problematic prompts.');
}
/**
* Build final LM input for step 4, including HARD RULES and scene info.
* The LM should return: { "videoPrompt": "..." }
*/
function buildLMInputFromRecordWithImageInfo(
genre: string,
subGenre: string,
finalScene: string,
chosenAction: string,
camera: string,
mainobject: string,
actiontype: string,
cameraworkType: string
) {
const accents = 'none';
const mood = 'n/a';
const lighting = 'n/a';
const style = 'n/a';
const lmInput = `
Return exactly one JSON object: { "videoPrompt": "..." } and nothing else.
Write "videoPrompt" in 100150 words, present tense, plain concrete language.
HARD RULES (must comply):
- One continuous shot ("one take", "oner"). Real-time 8 seconds. No edits.
- Fixed location and vantage. Do not change background or angle.
- Lens and focal length locked. No zooms, no close-ups that imply a lens change, no rack zoom.
- Camera motion: at most subtle pan/tilt/dolly within 1 meter while staying in the same spot.
- Keep framing consistent (e.g., medium-wide two-shot). No “another shot/meanwhile.”
- Describe: (1) main action, (2) framing & motion, (3) lighting & mood, (4) style & small accents.
- Use clear simple sentences. No metaphors or poetic language.
PROHIBITED WORDS/PHRASES (case-insensitive):
cut, cuts, cutting, quick cut, insert, macro insert, close-up, extreme close-up,
zoom, zooms, zooming, push-in, pull-out, whip, switch angle, change angle,
montage, cross-cut, smash cut, transition, meanwhile, later.
If proximity is needed, say: "the camera glides slightly closer while staying in the same position."
Here is information of the scene, please generate prompt for the video based on these information for key "videoPrompt":
Genre: ${genre}
Sub-Genre: ${subGenre}
Scene: ${finalScene}
Existing Action Field: ${chosenAction || 'n/a'}
Existing Camera Field: ${camera || 'static or subtle movement (stay within scene)'}
Detected Main Object: ${mainobject}
Suggested Action Type: ${actiontype}
Suggested Camera Work: ${cameraworkType}
Accents: ${accents}
Mood: ${mood}
Lighting: ${lighting}
Style: ${style}
`;
return lmInput;
}
main();

View File

@ -1,5 +1,6 @@
import { query } from './lib/mysql'; import { query } from './lib/mysql';
import { generateVideo } from './lib/video-generator'; import { generateVideo } from './lib/video-generator';
//import { generateVideo } from './lib/video-generator-text';
import { logger } from './lib/logger'; import { logger } from './lib/logger';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
@ -16,10 +17,10 @@ interface VideoRecord {
} }
const servers = [ const servers = [
{ /*{
baseUrl: process.env.SERVER1_COMFY_BASE_URL, baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR, outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
}, },*/
{ {
baseUrl: process.env.SERVER2_COMFY_BASE_URL, baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR, outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
@ -80,8 +81,6 @@ async function worker(server: any) {
const videosToProcess = (await query( const videosToProcess = (await query(
`SELECT * FROM video `SELECT * FROM video
WHERE video_prompt IS NOT NULL WHERE video_prompt IS NOT NULL
AND image_path IS NOT NULL
AND image_path != 'processing'
AND (video_path IS NULL OR video_path = '') AND (video_path IS NULL OR video_path = '')
ORDER BY RAND() LIMIT 1` ORDER BY RAND() LIMIT 1`
)) as VideoRecord[]; )) as VideoRecord[];

View File

@ -0,0 +1,704 @@
import puppeteer from 'puppeteer';
import type { Page } from 'puppeteer';
import dotenv from 'dotenv';
import * as fs from 'fs/promises';
import path from 'path';
import { spawn } from 'child_process';
dotenv.config();
/**
* Script: generate_pinterest_keywords.ts
*
* For each genre/subgenre pair (provided below) this script:
* - searches Pinterest for "genre subgenre"
* - scrolls the results page several times
* - collects up to 20 unique pin IDs from the page
* - outputs a JSON array to the console:
* [{ genre: "...", subGenre: "...", pinIds: ["id1","id2", ...] }, ...]
*
* Usage:
* - npx ts-node src/generate_pinterest_keywords.ts
* - or compile with tsc and run with node
*
* Notes:
* - Puppeteer will run headless by default. If you need to debug visually set headless: false.
* - Adjust SCROLL_ITERATIONS and MAX_PIN_IDS_PER_TERM if you want different behavior.
*/
const SCROLL_ITERATIONS = 8; // number of times to scroll (adjust if you want more)
const MAX_PIN_IDS_PER_TERM = 20; // target number of pin ids per genre/subgenre
const SCROLL_DELAY_MS = 900; // delay between scrolls
const searchList: { genre: string; subGenre: string }[] = [
/* { genre: "abstract", subGenre: "3D Renderings" },
{ genre: "abstract", subGenre: "Abstract Portraits" },
{ genre: "abstract", subGenre: "Collage" },
{ genre: "abstract", subGenre: "Color Explosions" },
{ genre: "abstract", subGenre: "cubic" },
{ genre: "abstract", subGenre: "Cubism" },
{ genre: "abstract", subGenre: "Digital Glitch" },
{ genre: "abstract", subGenre: "Fluid Paints" },
{ genre: "abstract", subGenre: "Fractals" },
{ genre: "abstract", subGenre: "Geometric Shapes" },
{ genre: "abstract", subGenre: "Graffiti" },
{ genre: "abstract", subGenre: "Impressionism" },
{ genre: "abstract", subGenre: "Kaleidoscopes" },
{ genre: "abstract", subGenre: "Light Art" },
{ genre: "abstract", subGenre: "Mandalas" },
{ genre: "abstract", subGenre: "Minimalism" },
{ genre: "abstract", subGenre: "Optical Illusions" },
{ genre: "abstract", subGenre: "particle" },
{ genre: "abstract", subGenre: "Pop Art" },
{ genre: "abstract", subGenre: "science" },
{ genre: "abstract", subGenre: "space" },
{ genre: "abstract", subGenre: "sphere" },
{ genre: "abstract", subGenre: "Street Art" },
{ genre: "abstract", subGenre: "Surrealism" },
{ genre: "abstract", subGenre: "Typography Art" },
{ genre: "abstruct", subGenre: "art" },
{ genre: "abstruct", subGenre: "colors" },
{ genre: "abstruct", subGenre: "particle" },
{ genre: "animals", subGenre: "Bats" },
{ genre: "animals", subGenre: "Bears" },
{ genre: "animals", subGenre: "Butterflies" },
{ genre: "animals", subGenre: "Camels" },
{ genre: "animals", subGenre: "Crocodiles" },
{ genre: "animals", subGenre: "Dolphins" },
{ genre: "animals", subGenre: "Eagles" },
{ genre: "animals", subGenre: "Elephants" },
{ genre: "animals", subGenre: "Foxes" },
{ genre: "animals", subGenre: "Giraffes" },
{ genre: "animals", subGenre: "Horses" },
{ genre: "animals", subGenre: "Lions" },
{ genre: "animals", subGenre: "Owls" },
{ genre: "animals", subGenre: "Pandas" },
{ genre: "animals", subGenre: "Penguins" },
{ genre: "animals", subGenre: "Seals" },
{ genre: "animals", subGenre: "Sharks" },
{ genre: "animals", subGenre: "Tigers" },
{ genre: "animals", subGenre: "Whales" },
{ genre: "animals", subGenre: "Wolves" },
{ genre: "architecture", subGenre: "luxury room" },
{ genre: "childhood", subGenre: "Adoption" },
{ genre: "childhood", subGenre: "Babies" },
{ genre: "childhood", subGenre: "Bedtime Stories" },
{ genre: "childhood", subGenre: "Birthday Parties" },
{ genre: "childhood", subGenre: "Children Playing" },
{ genre: "childhood", subGenre: "Family Dinners" },
{ genre: "childhood", subGenre: "Family Portraits" },
{ genre: "childhood", subGenre: "Graduations" },
{ genre: "childhood", subGenre: "Grandparents" },
{ genre: "childhood", subGenre: "Holiday Celebrations" },
{ genre: "childhood", subGenre: "Learning to Walk" },
{ genre: "childhood", subGenre: "Nursery" },
{ genre: "childhood", subGenre: "Parent-Teacher Meetings" },
{ genre: "childhood", subGenre: "Picnics" },
{ genre: "childhood", subGenre: "Pregnancy" },
{ genre: "childhood", subGenre: "School Activities" },
{ genre: "childhood", subGenre: "Siblings" },
{ genre: "childhood", subGenre: "Sports with Kids" },
{ genre: "childhood", subGenre: "Toys" },
{ genre: "childhood", subGenre: "Vacations" },
{ genre: "cinematic", subGenre: "Action Movies" },
{ genre: "cinematic", subGenre: "Animations" },
{ genre: "cinematic", subGenre: "Documentaries" },
{ genre: "cinematic", subGenre: "Experimental Cinema" },
{ genre: "cinematic", subGenre: "Fantasy Epics" },
{ genre: "cinematic", subGenre: "Historical Dramas" },
{ genre: "cinematic", subGenre: "Hollywood Blockbusters" },
{ genre: "cinematic", subGenre: "Horror Films" },
{ genre: "cinematic", subGenre: "Indie Films" },
{ genre: "cinematic", subGenre: "Mockumentaries" },
{ genre: "cinematic", subGenre: "Musicals" },
{ genre: "cinematic", subGenre: "Nature Documentaries" },
{ genre: "cinematic", subGenre: "Noir" },
{ genre: "cinematic", subGenre: "Romantic Comedies" },
{ genre: "cinematic", subGenre: "Sci-Fi Thrillers" },
{ genre: "cinematic", subGenre: "Short Films" },
{ genre: "cinematic", subGenre: "Silent Films" },
{ genre: "cinematic", subGenre: "Stop Motion" },
{ genre: "cinematic", subGenre: "Superhero Films" },
{ genre: "cinematic", subGenre: "Westerns" },
{ genre: "city", subGenre: "Bridges" },
{ genre: "city", subGenre: "Castles" },
{ genre: "city", subGenre: "Cathedrals" },
{ genre: "city", subGenre: "Factories" },
{ genre: "city", subGenre: "Futuristic Cities" },
{ genre: "city", subGenre: "Historic Towns" },
{ genre: "city", subGenre: "Libraries" },
{ genre: "city", subGenre: "Markets" },
{ genre: "city", subGenre: "Modern Plazas" },
{ genre: "city", subGenre: "Museums" },
{ genre: "city", subGenre: "Palaces" },
{ genre: "city", subGenre: "Residential Blocks" },
{ genre: "city", subGenre: "Skylines" },
{ genre: "city", subGenre: "Skyscrapers" },
{ genre: "city", subGenre: "Slums" },
{ genre: "city", subGenre: "Stadiums" },
{ genre: "city", subGenre: "Street Cafes" },
{ genre: "city", subGenre: "Urban Parks" },
{ genre: "fantasy", subGenre: "academia" },
{ genre: "fantasy", subGenre: "aesthetic" },
{ genre: "fantasy", subGenre: "art" },
{ genre: "fantasy", subGenre: "Crystal Caves" },
{ genre: "fantasy", subGenre: "Dark Castles" },
{ genre: "fantasy", subGenre: "dark ethereal" },
{ genre: "fantasy", subGenre: "darkacademia" },
{ genre: "fantasy", subGenre: "ddreamy room" },
{ genre: "fantasy", subGenre: "Dragon Realms" },
{ genre: "fantasy", subGenre: "dreamy room" },
{ genre: "fantasy", subGenre: "Elven Cities" },
{ genre: "fantasy", subGenre: "Enchanted Rivers" },
{ genre: "fantasy", subGenre: "Epic Battles" },
{ genre: "fantasy", subGenre: "ethereal" },
{ genre: "fantasy", subGenre: "Fairy Villages" },
{ genre: "fantasy", subGenre: "Floating Islands" },
{ genre: "fantasy", subGenre: "Ghostly Spirits" },
{ genre: "fantasy", subGenre: "illumication" },
{ genre: "fantasy", subGenre: "Knights" },
{ genre: "fantasy", subGenre: "landscape" },
{ genre: "fantasy", subGenre: "Magic Forests" },
{ genre: "fantasy", subGenre: "Magical Beasts" },
{ genre: "fantasy", subGenre: "Mystic Portals" },
{ genre: "fantasy", subGenre: "Mythical Weapons" },
{ genre: "fantasy", subGenre: "Queens and Kings" },
{ genre: "fantasy", subGenre: "Runes and Symbols" },
{ genre: "fantasy", subGenre: "Sacred Temples" },
{ genre: "fantasy", subGenre: "Shape-shifters" },
{ genre: "fantasy", subGenre: "Talking Animals" },
{ genre: "fantasy", subGenre: "Wizards" },
{ genre: "fashion", subGenre: "Accessories" },
{ genre: "fashion", subGenre: "Boho Style" },
{ genre: "fashion", subGenre: "Bridal Wear" },
{ genre: "fashion", subGenre: "Business Suits" },
{ genre: "fashion", subGenre: "Casual Wear" },
{ genre: "fashion", subGenre: "Cocktail Dresses" },
{ genre: "fashion", subGenre: "Cosplay" },
{ genre: "fashion", subGenre: "Evening Gowns" },
{ genre: "fashion", subGenre: "Hair Styling" },
{ genre: "fashion", subGenre: "Haute Couture" },
{ genre: "fashion", subGenre: "Makeup Styles" },
{ genre: "fashion", subGenre: "Pajamas" },
{ genre: "fashion", subGenre: "Runway Shows" },
{ genre: "fashion", subGenre: "School Uniforms" },
{ genre: "fashion", subGenre: "Shoes" },
{ genre: "fashion", subGenre: "Sportswear" },
{ genre: "fashion", subGenre: "Streetwear" },
{ genre: "fashion", subGenre: "Swimwear" },
{ genre: "fashion", subGenre: "Traditional Costumes" },
{ genre: "fashion", subGenre: "Vintage Outfits" },
{ genre: "food", subGenre: "Bakeries" },
{ genre: "food", subGenre: "BBQ" },
{ genre: "food", subGenre: "Breakfasts" },
{ genre: "food", subGenre: "Chocolate Making" },
{ genre: "food", subGenre: "Cocktails" },
{ genre: "food", subGenre: "Coffee Culture" },
{ genre: "food", subGenre: "Desserts" },
{ genre: "food", subGenre: "Farmers Market" },
{ genre: "food", subGenre: "Fine Dining" },
{ genre: "food", subGenre: "Food Trucks" },
{ genre: "food", subGenre: "Fruit Platters" },
{ genre: "food", subGenre: "Pasta Dishes" },
{ genre: "food", subGenre: "Pizza" },
{ genre: "food", subGenre: "Seafood" },
{ genre: "food", subGenre: "Spices and Herbs" },
{ genre: "food", subGenre: "Street Food" },
{ genre: "food", subGenre: "Sushi" },
{ genre: "food", subGenre: "Tea Ceremonies" },
{ genre: "food", subGenre: "Vegetarian Meals" },
{ genre: "food", subGenre: "Wine Tasting" },
{ genre: "history", subGenre: "African Tribes" },
{ genre: "history", subGenre: "Ancient Egypt" },
{ genre: "history", subGenre: "Celtic Legends" },
{ genre: "history", subGenre: "Chinese Dynasties" },
{ genre: "history", subGenre: "Greek Temples" },
{ genre: "history", subGenre: "Indian Kingdoms" },
{ genre: "history", subGenre: "Industrial Revolution" },
{ genre: "history", subGenre: "Mayan Ruins" },
{ genre: "history", subGenre: "Medieval Europe" },
{ genre: "history", subGenre: "Native American" },
{ genre: "history", subGenre: "Nomadic Life" },
{ genre: "history", subGenre: "Ottoman Empire" },
{ genre: "history", subGenre: "Renaissance" },
{ genre: "history", subGenre: "Samurai Japan" },
{ genre: "history", subGenre: "Traditional Festivals" },
{ genre: "history", subGenre: "Victorian Era" },
{ genre: "history", subGenre: "Viking Culture" },
{ genre: "history", subGenre: "World War Eras" },
{ genre: "horror", subGenre: "Abandoned Hospitals" },
{ genre: "horror", subGenre: "Ancient Tombs" },
{ genre: "horror", subGenre: "Blood Moons" },
{ genre: "horror", subGenre: "Creepy Dolls" },
{ genre: "horror", subGenre: "Curses" },
{ genre: "horror", subGenre: "Dark Alleys" },
{ genre: "horror", subGenre: "Demons" },
{ genre: "horror", subGenre: "Foggy Forests" },
{ genre: "horror", subGenre: "Ghosts" },
{ genre: "horror", subGenre: "Graveyards" },
{ genre: "horror", subGenre: "Haunted Houses" },
{ genre: "horror", subGenre: "Monsters" },
{ genre: "horror", subGenre: "Occult Rituals" },
{ genre: "horror", subGenre: "Possessions" },
{ genre: "horror", subGenre: "Scary Clowns" },
{ genre: "horror", subGenre: "Shadows" },
{ genre: "horror", subGenre: "Silent Villages" },
{ genre: "horror", subGenre: "Vampires" },
{ genre: "horror", subGenre: "Werewolves" },
{ genre: "horror", subGenre: "Witches" },
{ genre: "music", subGenre: "Ballet" },
{ genre: "music", subGenre: "Breakdance" },
{ genre: "music", subGenre: "Choirs" },
{ genre: "music", subGenre: "Classical Concerts" },
{ genre: "music", subGenre: "Dance Battles" },
{ genre: "music", subGenre: "DJ Performances" },
{ genre: "music", subGenre: "Drumming" },
{ genre: "music", subGenre: "Electronic Music" },
{ genre: "music", subGenre: "Flamenco" },
{ genre: "music", subGenre: "Folk Dance" },
{ genre: "music", subGenre: "Hip-Hop Dance" },
{ genre: "music", subGenre: "Jazz Clubs" },
{ genre: "music", subGenre: "K-Pop" },
{ genre: "music", subGenre: "Opera" },
{ genre: "music", subGenre: "Orchestras" },
{ genre: "music", subGenre: "Rock Bands" },
{ genre: "music", subGenre: "Salsa" },
{ genre: "music", subGenre: "Singing Solo" },
{ genre: "music", subGenre: "Street Dance" },
{ genre: "music", subGenre: "Tap Dance" },
{ genre: "nature", subGenre: "Aurora Skies" },
{ genre: "nature", subGenre: "Canyons" },
{ genre: "nature", subGenre: "Caves" },
{ genre: "nature", subGenre: "Cliffs" },
{ genre: "nature", subGenre: "Coral Reefs" },
{ genre: "nature", subGenre: "Deserts" },
{ genre: "nature", subGenre: "Forests" },
{ genre: "nature", subGenre: "Glaciers" },
{ genre: "nature", subGenre: "Lakes" },
{ genre: "nature", subGenre: "Meadows" },
{ genre: "nature", subGenre: "Mountains" },
{ genre: "nature", subGenre: "night sky" },
{ genre: "nature", subGenre: "Oceans" },
{ genre: "nature", subGenre: "Rainforest" },
{ genre: "nature", subGenre: "Rivers" },
{ genre: "nature", subGenre: "Savannah" },
{ genre: "nature", subGenre: "Storms" },
{ genre: "nature", subGenre: "Sunsets" },
{ genre: "nature", subGenre: "Volcanoes" },
{ genre: "nature", subGenre: "Waterfalls" },
{ genre: "nature", subGenre: "Wetlands" },
{ genre: "people", subGenre: "Artists" },
{ genre: "people", subGenre: "Celebrations" },
{ genre: "people", subGenre: "Children" },
{ genre: "people", subGenre: "Commuting" },
{ genre: "people", subGenre: "Craftsmen" },
{ genre: "people", subGenre: "Daily Life" },
{ genre: "people", subGenre: "Elderly" },
{ genre: "people", subGenre: "Family Moments" },
{ genre: "people", subGenre: "Farm Life" },
{ genre: "people", subGenre: "Festivals" },
{ genre: "people", subGenre: "Fishing Villages" },
{ genre: "people", subGenre: "Friendship" },
{ genre: "people", subGenre: "Markets" },
{ genre: "people", subGenre: "Relaxing" },
{ genre: "people", subGenre: "Romantic Dates" },
{ genre: "people", subGenre: "School Days" },
{ genre: "people", subGenre: "Shopping" },
{ genre: "people", subGenre: "Street Life" },
{ genre: "people", subGenre: "Travelers" },
{ genre: "people", subGenre: "Workplaces" },
{ genre: "romance", subGenre: "Anger" },
{ genre: "romance", subGenre: "Anniversaries" },
{ genre: "romance", subGenre: "Arguments" },
{ genre: "romance", subGenre: "Comforting" },
{ genre: "romance", subGenre: "Confessions" },
{ genre: "romance", subGenre: "Family Love" },
{ genre: "romance", subGenre: "Farewells" },
{ genre: "romance", subGenre: "First Dates" },
{ genre: "romance", subGenre: "Friendship" },
{ genre: "romance", subGenre: "Heartbreak" },
{ genre: "romance", subGenre: "Hugs" },
{ genre: "romance", subGenre: "Joy" },
{ genre: "romance", subGenre: "Kisses" },
{ genre: "romance", subGenre: "Laughter" },
{ genre: "romance", subGenre: "Loneliness" },
{ genre: "romance", subGenre: "Reunions" },
{ genre: "romance", subGenre: "Sadness" },
{ genre: "romance", subGenre: "Surprise" },
{ genre: "romance", subGenre: "wedding" },
{ genre: "romance", subGenre: "Weddings" },
{ genre: "sci-fi", subGenre: "AI Entities" },
{ genre: "sci-fi", subGenre: "Aliens" },
{ genre: "sci-fi", subGenre: "Androids" },
{ genre: "sci-fi", subGenre: "Clones" },
{ genre: "sci-fi", subGenre: "Colonies on Mars" },
{ genre: "sci-fi", subGenre: "Cyberpunk Cities" },
{ genre: "sci-fi", subGenre: "Exosuits" },
{ genre: "sci-fi", subGenre: "Futuristic Weapons" },
{ genre: "sci-fi", subGenre: "Galactic Battles" },
{ genre: "sci-fi", subGenre: "Genetic Labs" },
{ genre: "sci-fi", subGenre: "Holograms" },
{ genre: "sci-fi", subGenre: "Hover Cars" },
{ genre: "sci-fi", subGenre: "Nanotechnology" },
{ genre: "sci-fi", subGenre: "Post-Apocalypse" },
{ genre: "sci-fi", subGenre: "Robots" },
{ genre: "sci-fi", subGenre: "Space Stations" },
{ genre: "sci-fi", subGenre: "Time Travel" },
{ genre: "sci-fi", subGenre: "Virtual Reality" },
{ genre: "sci-fi", subGenre: "Wormholes" },
{ genre: "space", subGenre: "Asteroids" },
{ genre: "space", subGenre: "Aurora" },
{ genre: "space", subGenre: "Black Holes" },
{ genre: "space", subGenre: "Comets" },
{ genre: "space", subGenre: "Cosmic Dust" },
{ genre: "space", subGenre: "Deep Space" },
{ genre: "space", subGenre: "Eclipses" },
{ genre: "space", subGenre: "Exoplanets" },
{ genre: "space", subGenre: "Galaxies" },
{ genre: "space", subGenre: "Meteor Showers" },
{ genre: "space", subGenre: "Moons" },
{ genre: "space", subGenre: "Nebulae" },
{ genre: "space", subGenre: "Observatories" },
{ genre: "space", subGenre: "Planets" },
{ genre: "space", subGenre: "Rocket Launches" },
{ genre: "space", subGenre: "Satellites" },
{ genre: "space", subGenre: "Spacewalks" },
{ genre: "space", subGenre: "Stars" },
{ genre: "space", subGenre: "Supernovas" },
{ genre: "space", subGenre: "Telescopes" },
{ genre: "sports", subGenre: "Archery" },
{ genre: "sports", subGenre: "Baseball" },
{ genre: "sports", subGenre: "Basketball" },
{ genre: "sports", subGenre: "Boxing" },
{ genre: "sports", subGenre: "Climbing" },
{ genre: "sports", subGenre: "Cycling" },
{ genre: "sports", subGenre: "Fencing" },
{ genre: "sports", subGenre: "Gymnastics" },
{ genre: "sports", subGenre: "Horse Riding" },
{ genre: "sports", subGenre: "Martial Arts" },
{ genre: "sports", subGenre: "Rowing" },
{ genre: "sports", subGenre: "Running" },
{ genre: "sports", subGenre: "Skateboarding" },
{ genre: "sports", subGenre: "Skiing" },
{ genre: "sports", subGenre: "Snowboarding" },
{ genre: "sports", subGenre: "Soccer" },
{ genre: "sports", subGenre: "Surfing" },
{ genre: "sports", subGenre: "Swimming" },
{ genre: "sports", subGenre: "Tennis" },
{ genre: "sports", subGenre: "Yoga" },
{ genre: "technology", subGenre: "3D Printing" },
{ genre: "technology", subGenre: "Artificial Intelligence" },
{ genre: "technology", subGenre: "Augmented Reality" },
{ genre: "technology", subGenre: "Autonomous Cars" },
{ genre: "technology", subGenre: "Biotech" },
{ genre: "technology", subGenre: "Cyber Security" },
{ genre: "technology", subGenre: "Data Centers" },
{ genre: "technology", subGenre: "Digital Currency" },
{ genre: "technology", subGenre: "Drones" },
{ genre: "technology", subGenre: "Futuristic Homes" },
{ genre: "technology", subGenre: "Green Tech" },
{ genre: "technology", subGenre: "Nanobots" },
{ genre: "technology", subGenre: "Quantum Computing" },
{ genre: "technology", subGenre: "Robotics" },
{ genre: "technology", subGenre: "Smart Cities" },
{ genre: "technology", subGenre: "Smart Farms" },
{ genre: "technology", subGenre: "Space Elevators" },
{ genre: "technology", subGenre: "Surveillance Systems" },
{ genre: "technology", subGenre: "VR Worlds" },
{ genre: "technology", subGenre: "Wearables" },
{ genre: "travel", subGenre: "Backpacking" },
{ genre: "travel", subGenre: "Camping" },
{ genre: "travel", subGenre: "City Tours" },
{ genre: "travel", subGenre: "Cruises" },
{ genre: "travel", subGenre: "Cultural Trips" },
{ genre: "travel", subGenre: "Desert Journeys" },
{ genre: "travel", subGenre: "Diving Trips" },
{ genre: "travel", subGenre: "Exploring Ruins" },
{ genre: "travel", subGenre: "Festivals Abroad" },
{ genre: "travel", subGenre: "Glamping" },
{ genre: "travel", subGenre: "Hiking" },
{ genre: "travel", subGenre: "Hot Air Balloons" },
{ genre: "travel", subGenre: "Island Hopping" },
{ genre: "travel", subGenre: "Jungle Treks" },
{ genre: "travel", subGenre: "Motorbike Tours" },
{ genre: "travel", subGenre: "Mountain Climbing" },
{ genre: "travel", subGenre: "Polar Expeditions" },
{ genre: "travel", subGenre: "Road Trips" },
{ genre: "travel", subGenre: "Safari" },
{ genre: "travel", subGenre: "Train Journeys" },
{ genre: "work", subGenre: "Actors" },
{ genre: "work", subGenre: "Artists" },
{ genre: "work", subGenre: "Athletes" },
{ genre: "work", subGenre: "Chefs" },
{ genre: "work", subGenre: "Craftsmen" },
{ genre: "girl", subGenre: "Casual Dresses" },
{ genre: "girl", subGenre: "Mini Skirts" },
{ genre: "girl", subGenre: "Long Skirts" },
{ genre: "girl", subGenre: "Evening Gowns" },
{ genre: "girl", subGenre: "School Uniforms" },
{ genre: "girl", subGenre: "Business Suits" },
{ genre: "girl", subGenre: "Cocktail Dresses" },
{ genre: "girl", subGenre: "Boho Outfits" },
{ genre: "girl", subGenre: "Streetwear" },
{ genre: "girl", subGenre: "Traditional Costumes" },
{ genre: "girl", subGenre: "Party Dresses" },
{ genre: "girl", subGenre: "Swimwear" },
{ genre: "girl", subGenre: "Sportswear" },
{ genre: "girl", subGenre: "Lingerie" },
{ genre: "girl", subGenre: "Pajamas" },
{ genre: "girl", subGenre: "Gothic Outfits" },
{ genre: "girl", subGenre: "Vintage Dresses" },
{ genre: "girl", subGenre: "Winter Coats" },
{ genre: "girl", subGenre: "Summer Outfits" },
{ genre: "girl", subGenre: "Festival Outfits" },
{ genre: "woman", subGenre: "Evening Gowns" },
{ genre: "woman", subGenre: "Cocktail Dresses" },
{ genre: "woman", subGenre: "Ball Gowns" },
{ genre: "woman", subGenre: "Business Attire" },
{ genre: "woman", subGenre: "Formal Suits" },
{ genre: "woman", subGenre: "Luxury Lingerie" },
{ genre: "woman", subGenre: "Opera Dresses" },
{ genre: "woman", subGenre: "Elegant Skirts" },
{ genre: "woman", subGenre: "Designer Outfits" },
{ genre: "woman", subGenre: "Classic Black Dress" },
{ genre: "woman", subGenre: "High Fashion Couture" },
{ genre: "woman", subGenre: "Vintage Elegance" },
{ genre: "woman", subGenre: "Wedding Dresses" },
{ genre: "woman", subGenre: "Chic Office Wear" },
{ genre: "woman", subGenre: "Luxury Coats" },
{ genre: "woman", subGenre: "Formal Evening Suits" },
{ genre: "woman", subGenre: "Silk Dresses" },
{ genre: "woman", subGenre: "Red Carpet Outfits" },
{ genre: "woman", subGenre: "Classic Tailored Wear" },
{ genre: "woman", subGenre: "Luxury Resort Wear" }
{ "genre": "woman", "subGenre": "Casual Dresses" },
{ "genre": "woman", "subGenre": "Jeans and Blouses" },
{ "genre": "woman", "subGenre": "Maxi Skirts" },
{ "genre": "woman", "subGenre": "Casual Office Wear" },
{ "genre": "woman", "subGenre": "Cardigans with Skirts" },
{ "genre": "woman", "subGenre": "Casual Jumpsuits" },
{ "genre": "woman", "subGenre": "Casual Knitwear" },
{ "genre": "woman", "subGenre": "Linen Outfits" },
{ "genre": "woman", "subGenre": "Weekend Outfits" },
{ "genre": "woman", "subGenre": "Street Casual" },
{ "genre": "woman", "subGenre": "Boho Casual" },
{ "genre": "woman", "subGenre": "Smart Casual" },
{ "genre": "woman", "subGenre": "Denim Skirts" },
{ "genre": "woman", "subGenre": "Casual Midi Dresses" },
{ "genre": "woman", "subGenre": "Summer Casual" },
{ "genre": "woman", "subGenre": "Casual Sweaters" },
{ "genre": "woman", "subGenre": "Casual Jackets" },
{ "genre": "woman", "subGenre": "Everyday Casual Wear" },
{ "genre": "woman", "subGenre": "Relaxed Home Wear" },
{ "genre": "woman", "subGenre": "Travel Casual" },
{ "genre": "girl", "subGenre": "Casual Mini Skirts" },
{ "genre": "girl", "subGenre": "T-Shirts and Shorts" },
{ "genre": "girl", "subGenre": "Casual Sundresses" },
{ "genre": "girl", "subGenre": "Sweaters with Skirts" },
{ "genre": "girl", "subGenre": "Casual Hoodies" },
{ "genre": "girl", "subGenre": "Casual Denim" },
{ "genre": "girl", "subGenre": "Casual Rompers" },
{ "genre": "girl", "subGenre": "School Casual" },
{ "genre": "girl", "subGenre": "Casual Tops and Skirts" },
{ "genre": "girl", "subGenre": "Leggings and Tees" },
{ "genre": "girl", "subGenre": "Casual Tank Tops" },
{ "genre": "girl", "subGenre": "Sporty Casual" },
{ "genre": "girl", "subGenre": "Casual Pajamas" },
{ "genre": "girl", "subGenre": "Casual Jackets" },
{ "genre": "girl", "subGenre": "Simple Dresses" },
{ "genre": "girl", "subGenre": "Casual Streetwear" },
{ "genre": "girl", "subGenre": "Casual Overalls" },
{ "genre": "girl", "subGenre": "Casual Knit Tops" },
{ "genre": "girl", "subGenre": "Casual Party Wear" },
{ "genre": "girl", "subGenre": "Weekend Casual" },
{ "genre": "girl", "subGenre": "Anime School Uniform" },
{ "genre": "girl", "subGenre": "Magical Girl Costume" },
{ "genre": "girl", "subGenre": "Catgirl Outfit" },
{ "genre": "girl", "subGenre": "Maid Outfit" },
{ "genre": "girl", "subGenre": "Nurse Uniform" },
{ "genre": "girl", "subGenre": "Sailor Suit" },
{ "genre": "girl", "subGenre": "Fantasy Elf Costume" },
{ "genre": "girl", "subGenre": "Vampire Girl Outfit" },
{ "genre": "girl", "subGenre": "Gothic Lolita Dress" },
{ "genre": "girl", "subGenre": "Princess Dress" },
{ "genre": "girl", "subGenre": "Warrior Girl Armor" },
{ "genre": "girl", "subGenre": "Cyberpunk Outfit" },
{ "genre": "girl", "subGenre": "Steampunk Girl Costume" },
{ "genre": "girl", "subGenre": "Fairy Wings Costume" },
{ "genre": "girl", "subGenre": "Idol Singer Outfit" },
{ "genre": "girl", "subGenre": "Bunny Girl Costume" },
{ "genre": "girl", "subGenre": "Magical Witch Outfit" },
{ "genre": "girl", "subGenre": "Samurai Girl Costume" },
{ "genre": "girl", "subGenre": "Succubus Outfit" },
{ "genre": "girl", "subGenre": "Video Game Heroine Cosplay" }
{ "genre": "epic", "subGenre": "woman" },
{ "genre": "epic", "subGenre": "girl" },
{ "genre": "epic", "subGenre": "human" },
{ "genre": "epic", "subGenre": "man" },
{ "genre": "epic", "subGenre": "architecture" },
{ "genre": "epic", "subGenre": "animals" },
{ "genre": "epic", "subGenre": "ethereal" },
{ "genre": "epic", "subGenre": "gothic" },
{ "genre": "epic", "subGenre": "dark" },
{ "genre": "epic", "subGenre": "space" },
{ "genre": "epic", "subGenre": "scene" },
{ "genre": "epic", "subGenre": "black" },
{ "genre": "epic", "subGenre": "colorful" },
{ "genre": "epic", "subGenre": "bright" },
{ "genre": "epic", "subGenre": "abstract" },
{ "genre": "epic", "subGenre": "abstract color" },
{ "genre": "epic", "subGenre": "room" },
{ "genre": "epic", "subGenre": "building" },
{ "genre": "epic", "subGenre": "wizard" },
{ "genre": "epic", "subGenre": "future" },
{ "genre": "epic", "subGenre": "landscape" },
{ "genre": "epic", "subGenre": "stars" },
*/
{ "genre": "dance", "subGenre": "hiphop horizontal" },
{ "genre": "dance", "subGenre": "hiphop group horizontal" },
{ "genre": "dance", "subGenre": "tiktok horizontal" },
{ "genre": "dance", "subGenre": "female street horizontal" },
{ "genre": "dance", "subGenre": "male street horizontal" },
{ "genre": "dance", "subGenre": "idol horizontal" },
];
function extractPinIdFromHref(href: string): string | null {
// hrefs look like https://www.pinterest.com/pin/123456789012345678/...
const m = href.match(/\/pin\/([^\/\?]+)/);
return m ? m[1] : null;
}
async function collectPinIdsForSearch(page: Page, query: string): Promise<string[]> {
const pinIds = new Set<string>();
const searchUrl = `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(query)}`;
await page.goto(searchUrl, { waitUntil: 'networkidle2', timeout: 60000 });
// initial collect
const collect = async () => {
const hrefs = await page.$$eval('a', anchors => (anchors as any[]).map(a => (a as any).href)) as string[];
for (const href of hrefs) {
if (!href) continue;
if (href.includes('/pin/')) {
const id = href.match(/\/pin\/([^\/\?]+)/);
if (id && id[1]) pinIds.add(id[1]);
if (pinIds.size >= MAX_PIN_IDS_PER_TERM) break;
}
}
};
await collect();
// Scroll a few times to load more results
for (let i = 0; i < SCROLL_ITERATIONS && pinIds.size < MAX_PIN_IDS_PER_TERM; i++) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await new Promise(resolve => setTimeout(resolve, SCROLL_DELAY_MS + Math.floor(Math.random() * 800)));
await collect();
}
return Array.from(pinIds).slice(0, MAX_PIN_IDS_PER_TERM);
}
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
const results: { genre: string; subGenre: string; pinIds: string[] }[] = [];
for (const item of searchList) {
const term = `${item.genre} ${item.subGenre}`;
try {
console.log(`Searching Pinterest for: "${term}"`);
const pinIds = await collectPinIdsForSearch(page, term);
console.log(` -> Found ${pinIds.length} pinIds for "${term}"`);
results.push({ genre: item.genre, subGenre: item.subGenre, pinIds });
// small delay between queries to avoid being throttled
await new Promise(resolve => setTimeout(resolve, 600 + Math.floor(Math.random() * 1000)));
} catch (err) {
console.error(`Error collecting pins for "${term}":`, err);
results.push({ genre: item.genre, subGenre: item.subGenre, pinIds: [] });
}
}
await browser.close();
// Merge results with existing src/pinterest_keywords.json (if present),
// then write merged data to both src/pinterest_keywords.json and generated/pinterest_keywords.json
const srcPath = path.join(process.cwd(), 'src', 'pinterest_keywords.json');
const outDir = path.join(process.cwd(), 'generated');
try {
await fs.mkdir(outDir, { recursive: true });
// load existing entries from src/pinterest_keywords.json (if available)
let existing: { genre: string; subGenre: string; pinIds: string[] }[] = [];
try {
const raw = await fs.readFile(srcPath, 'utf-8');
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) existing = parsed;
} catch (e) {
// file missing or invalid JSON -> start with empty existing array
existing = [];
}
// Build map keyed by genre||subGenre to merge pinIds (dedupe)
const map = new Map<string, { genre: string; subGenre: string; pinIds: string[] }>();
for (const e of existing) {
const key = `${e.genre}||${e.subGenre}`;
map.set(key, { genre: e.genre, subGenre: e.subGenre, pinIds: Array.from(new Set(e.pinIds || [])) });
}
for (const r of results) {
const key = `${r.genre}||${r.subGenre}`;
const existingEntry = map.get(key);
if (existingEntry) {
// preserve existing order, then append any new ids from r
const set = new Set(existingEntry.pinIds);
for (const id of r.pinIds || []) set.add(id);
existingEntry.pinIds = Array.from(set);
} else {
map.set(key, { genre: r.genre, subGenre: r.subGenre, pinIds: Array.from(new Set(r.pinIds || [])) });
}
}
const merged = Array.from(map.values());
// Write merged JSON to src and generated folder
const writePromises = [
fs.writeFile(srcPath, JSON.stringify(merged, null, 2), 'utf-8'),
fs.writeFile(path.join(outDir, 'pinterest_keywords.json'), JSON.stringify(merged, null, 2), 'utf-8')
];
await Promise.all(writePromises);
console.log(`Saved ${merged.length} entries to ${srcPath} and ${path.join(outDir, 'pinterest_keywords.json')}`);
} catch (err) {
console.error('Failed to write output file:', err);
// Fallback to printing JSON to stdout
console.log(JSON.stringify(results, null, 2));
}
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,4 @@
{
"initialImage": "This image shows a girl with long hair sitting cross-legged in meditation on a colorful field that covers the curve of an Earth-like sphere. The vibrant ground is dotted with glowing hues of green, purple, and pink, resembling a dreamlike meadow. Behind her, radiant light shines, highlighting her silhouette and giving her a divine presence. Above, the vast cosmos unfolds—swirling nebulae in brilliant blues, pinks, and purples fill the sky, while planets and celestial spheres float gracefully in space. Below, layers of glowing clouds and reflective water mirror the cosmic colors. The atmosphere is mystical, serene, and transcendent, blending nature with the universe.",
"videoPrompt": "A girl with long hair sits cross-legged in meditation on a colorful glowing field, illuminated by soft radiant light. She remains completely still, serene, and peaceful.Camera is rotating slowly around her in right direction keep same distance. The surface of water is waving gently, reflecting the vibrant colors of the sky and the glowing field. The cosmic background with swirling nebulae, planets, and stars remains static, creating a mystical and tranquil atmosphere. The overall scene is ethereal and dreamlike, with a harmonious blend of nature and the universe."
}

View File

@ -0,0 +1,395 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { spawn } from 'child_process';
import { logger } from '../lib/logger';
import { generateImage } from '../lib/image-generator';
import { generateVideo } from '../lib/video-generator';
dotenv.config();
type Size = { width: number; height: number };
interface InfinitySceneConfig {
initialImage: string;
videoPrompt: string;
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 720, height: 1280 };
const GENERATED_DIR = path.resolve('generated');
/**
* Load ComfyUI servers from env:
* - SERVER1_COMFY_BASE_URL, SERVER1_COMFY_OUTPUT_DIR
* - SERVER2_COMFY_BASE_URL, SERVER2_COMFY_OUTPUT_DIR
* (inputDir is inferred by replacing "output" with "input")
*/
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},
]
.filter((s) => !!s.baseUrl && !!s.outputDir)
.map((s) => ({
...s,
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
async function copyImageToAllServerInputs(servers: Server[], localGeneratedImagePath: string): Promise<string> {
const fileName = path.basename(localGeneratedImagePath);
for (const s of servers) {
if (!s.inputDir) continue;
const dest = path.join(s.inputDir, fileName);
try {
await fs.mkdir(s.inputDir, { recursive: true });
await fs.copyFile(localGeneratedImagePath, dest);
logger.debug(`Copied ${fileName} to ${s.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy ${fileName} to ${s.name} input: ${err}`);
}
}
return fileName; // name used by Comfy workflow inputs
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function fileExists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
/** ---------- ffmpeg / ffprobe helpers ---------- */
function runFfmpeg(args: string[], { cwd }: { cwd?: string } = {}): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn('ffmpeg', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
let stderr = '';
proc.stdout.on('data', (d) => logger.debug(d.toString()));
proc.stderr.on('data', (d) => {
const msg = d.toString();
stderr += msg;
logger.debug(msg); // ffmpeg prints progress to stderr
});
proc.on('error', (err: any) => reject(err));
proc.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`ffmpeg exited with code ${code}. ${stderr.slice(0, 1000)}`));
});
});
}
function runFfprobe(args: string[], { cwd }: { cwd?: string } = {}): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const proc = spawn('ffprobe', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
proc.stdout.on('data', (d) => (stdout += d.toString()));
proc.stderr.on('data', (d) => (stderr += d.toString()));
proc.on('error', (err) => reject(err));
proc.on('close', (code) => {
if (code === 0) resolve({ stdout, stderr });
else reject(new Error(`ffprobe exited with code ${code}. ${stderr.slice(0, 1000)}`));
});
});
}
/**
* Try to get precise total frame count using ffprobe.
* 1) nb_read_frames (requires -count_frames, may be slow but exact)
* 2) nb_frames (container-level, sometimes missing)
* Returns undefined if both unavailable.
*/
async function getVideoFrameCount(inputVideoPath: string): Promise<number | undefined> {
// Attempt 1: nb_read_frames
try {
const { stdout } = await runFfprobe([
'-v', 'error',
'-count_frames',
'-select_streams', 'v:0',
'-show_entries', 'stream=nb_read_frames',
'-of', 'default=nokey=1:noprint_wrappers=1',
inputVideoPath,
]);
const n = parseInt(stdout.trim(), 10);
if (Number.isFinite(n) && n > 0) return n;
} catch (e) {
logger.debug(`ffprobe nb_read_frames failed: ${e instanceof Error ? e.message : e}`);
}
// Attempt 2: nb_frames
try {
const { stdout } = await runFfprobe([
'-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'stream=nb_frames',
'-of', 'default=nokey=1:noprint_wrappers=1',
inputVideoPath,
]);
const n = parseInt(stdout.trim(), 10);
if (Number.isFinite(n) && n > 0) return n;
} catch (e) {
logger.debug(`ffprobe nb_frames failed: ${e instanceof Error ? e.message : e}`);
}
return undefined;
}
/**
* Fallback when frame count is unavailable:
* - Get duration (sec)
* - Seek extremely near the end with -sseof (negative seek from EOF)
* - Reverse that tiny tail and grab the first decoded frame
*/
async function extractLastFrameFallback(inputVideoPath: string, outputImagePath: string): Promise<void> {
await fs.mkdir(path.dirname(outputImagePath), { recursive: true });
// Read duration to decide a safe small tail (0.2s or 1% of duration)
let tail = 0.2;
try {
const { stdout } = await runFfprobe([
'-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'format=duration',
'-of', 'default=nokey=1:noprint_wrappers=1',
inputVideoPath,
]);
const dur = parseFloat(stdout.trim());
if (Number.isFinite(dur) && dur > 0) {
tail = Math.max(0.05, Math.min(0.5, dur * 0.01)); // 1% (min 0.05s, max 0.5s)
}
} catch { /* ignore */ }
const args = [
'-y',
'-sseof', `-${tail}`,
'-i', inputVideoPath,
'-vf', 'reverse',
'-vframes', '1',
'-q:v', '2',
outputImagePath,
];
await runFfmpeg(args);
}
/**
* Extract the last frame of a video into a PNG, accurately.
* Preferred: precise frame index with select=eq(n\,last)
* Fallback: tiny tail + reverse (handles weird containers/codecs)
*/
async function extractLastFrameAccurate(inputVideoPath: string, outputImagePath: string): Promise<void> {
const total = await getVideoFrameCount(inputVideoPath);
if (total && total > 0) {
const last = Math.max(0, total - 1);
await fs.mkdir(path.dirname(outputImagePath), { recursive: true });
const args = [
'-y',
'-i', inputVideoPath,
'-vf', `select=eq(n\\,${last})`,
'-vframes', '1',
'-q:v', '2',
outputImagePath,
];
try {
await runFfmpeg(args);
return;
} catch (e) {
logger.warn(`select by frame index failed, falling back: ${e instanceof Error ? e.message : e}`);
}
}
await extractLastFrameFallback(inputVideoPath, outputImagePath);
}
/**
* Concatenate two videos into a single MP4.
* First try concat demuxer with stream copy (fast, no re-encode).
* If it fails, fall back to re-encoding with concat filter.
*/
async function concatVideosFFmpeg(input1: string, input2: string, outputPath: string): Promise<void> {
await fs.mkdir(path.dirname(outputPath), { recursive: true });
// Try concat demuxer with copy (requires same codecs/params)
const listTxtPath = path.join(path.dirname(outputPath), `concat_${Date.now()}.txt`);
const listContent = `file '${path.resolve(input1).replace(/'/g, "'\\''")}'\nfile '${path.resolve(input2).replace(/'/g, "'\\''")}'\n`;
await fs.writeFile(listTxtPath, listContent, 'utf-8');
try {
await runFfmpeg(['-y', '-f', 'concat', '-safe', '0', '-i', listTxtPath, '-c', 'copy', outputPath]);
} catch (e) {
logger.warn(`Concat with -c copy failed, falling back to re-encode: ${e instanceof Error ? e.message : e}`);
// Fallback: re-encode with concat filter, video only
await runFfmpeg([
'-y',
'-i', input1,
'-i', input2,
'-filter_complex', 'concat=n=2:v=1:a=0',
'-c:v', 'libx264',
'-crf', '18',
'-preset', 'veryfast',
'-pix_fmt', 'yuv420p',
outputPath,
]);
} finally {
try { await fs.unlink(listTxtPath); } catch { /* ignore */ }
}
}
async function main() {
try {
await ensureDirs();
// Load scene config
const sceneRaw = await fs.readFile(path.resolve('src/infinityvideo_generator/scene.json'), 'utf-8');
const scene: InfinitySceneConfig = JSON.parse(sceneRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Optional limiter (env MAX_LOOPS). If not set, loop infinitely.
const MAX_LOOPS = process.env.MAX_LOOPS ? Math.max(0, parseInt(process.env.MAX_LOOPS, 10)) : undefined;
// Session identifiers
const sessionId = Date.now().toString(36);
let rrIndex = 0; // round-robin index across servers
let iteration = 0;
// Step 2: Determine initial image (use existing file if provided)
const providedInitialImagePath = path.resolve('src/infinityvideo_generator/initial_image.png');
let initialImagePath: string;
let initialImageName: string;
if (await fileExists(providedInitialImagePath)) {
initialImagePath = providedInitialImagePath;
initialImageName = path.basename(initialImagePath);
logger.info(`Using existing initial image at ${initialImagePath}`);
} else {
initialImageName = `infinity_${sessionId}_i${iteration}.png`;
const serverForInitialImage = pickServer(servers, rrIndex++);
logger.info(`Generating initial image (${initialImageName}) on ${serverForInitialImage.name} (flux)...`);
initialImagePath = await generateImage(
scene.initialImage,
initialImageName,
serverForInitialImage.baseUrl!,
serverForInitialImage.outputDir!,
'flux',
DEFAULT_SIZE,
);
logger.info(`Initial image generated: ${initialImagePath}`);
}
// Step 3: Copy image to input folders and generate first video
const imageNameForComfy = await copyImageToAllServerInputs(servers, initialImagePath);
const firstVideoName = initialImageName.replace(/\.png$/i, `_v${iteration}.mp4`);
const serverForFirstVideo = pickServer(servers, rrIndex++);
logger.info(`Generating first video (${firstVideoName}) on ${serverForFirstVideo.name} using ${imageNameForComfy}...`);
// Use "light" workflow and short length similar to musicspot videos
let currentVideoPath = await generateVideo(
scene.videoPrompt,
imageNameForComfy,
firstVideoName,
serverForFirstVideo.baseUrl!,
serverForFirstVideo.outputDir!,
DEFAULT_SIZE,
true,
true
);
logger.info(`First video generated: ${currentVideoPath}`);
// Loop:
// 4) Extract last frame (accurate)
// 5) Generate a new video from that image
// 6) Concat current video + new video => becomes the new "current" video
while (true) {
if (MAX_LOOPS !== undefined && iteration >= MAX_LOOPS) {
logger.info(`Reached MAX_LOOPS=${MAX_LOOPS}. Stopping.`);
break;
}
iteration += 1;
const lastFrameName = `infinity_${sessionId}_lastframe_${iteration}.png`;
const lastFramePath = path.join(GENERATED_DIR, lastFrameName);
logger.info(`Extracting last frame (accurate) from ${currentVideoPath} -> ${lastFramePath}`);
await extractLastFrameAccurate(currentVideoPath, lastFramePath);
const lastFrameExists = await fileExists(lastFramePath);
if (!lastFrameExists) {
throw new Error(`Failed to extract last frame to ${lastFramePath}`);
}
// Copy to server inputs
const frameNameForComfy = await copyImageToAllServerInputs(servers, lastFramePath);
// Generate new video from last frame
const newVideoName = `infinity_${sessionId}_video_${iteration}.mp4`;
const serverForVideo = pickServer(servers, rrIndex++);
logger.info(`Generating new video (${newVideoName}) on ${serverForVideo.name} using ${frameNameForComfy}...`);
const newVideoPath = await generateVideo(
scene.videoPrompt,
frameNameForComfy,
newVideoName,
serverForVideo.baseUrl!,
serverForVideo.outputDir!,
DEFAULT_SIZE,
true,
true
);
logger.info(`New segment generated: ${newVideoPath}`);
// Concat current + new => new current
const concatenatedName = `infinity_${sessionId}_concat_${iteration}.mp4`;
const concatenatedPath = path.join(GENERATED_DIR, concatenatedName);
logger.info(`Concatenating videos: [${currentVideoPath}] + [${newVideoPath}] -> ${concatenatedPath}`);
await concatVideosFFmpeg(currentVideoPath, newVideoPath, concatenatedPath);
logger.info(`Concatenated video: ${concatenatedPath}`);
// Set as the new current video for next loop
currentVideoPath = concatenatedPath;
}
logger.info('Infinity video generation finished.');
} catch (err) {
logger.error('Fatal error in infinity video generator:', err);
}
}
main().catch((err) => {
logger.error('Unhandled error:', err);
});

View File

@ -88,3 +88,78 @@ export async function downloadPinterestImages(keyword: string, numberOfPages: nu
logger.debug('Done.'); logger.debug('Done.');
return downloadedImagePaths; return downloadedImagePaths;
} }
export async function downloadImagesFromPinterestPin(pinUrl: string, scrollCount: number = 5): Promise<string[]> {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
const downloadedImagePaths: string[] = [];
const downloadedUrls = new Set<string>();
try {
await page.goto(pinUrl, { waitUntil: 'networkidle2' });
logger.debug(`Navigated to pin page: ${pinUrl}`);
const pageImageUrls = new Set<string>();
for (let i = 0; i < scrollCount; i++) {
logger.debug(`Scrolling page ${i + 1}/${scrollCount} for ${pinUrl}...`);
const imageUrlsOnPage = await page.evaluate(() => {
const images = Array.from(document.querySelectorAll('img[src*="i.pinimg.com"]'));
const urls = images.map(img => {
const image = img as HTMLImageElement;
if (image.srcset) {
const sources = image.srcset.split(',').map(s => s.trim().split(' '));
const fourXSource = sources.find(s => s[1] === '4x');
if (fourXSource) {
return fourXSource[0];
}
}
return null; // Return null if no 4x source is found
});
return urls.filter((url): url is string => url !== null); // Filter out nulls and assert type
});
for (const url of imageUrlsOnPage) {
pageImageUrls.add(url);
}
const previousHeight = await page.evaluate('document.body.scrollHeight');
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
try {
await page.waitForFunction(`document.body.scrollHeight > ${previousHeight}`, { timeout: 10000 });
} catch (e) {
logger.debug('No more content to load on this page.');
break;
}
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for images to load
}
let imageCount = 0;
for (const imageUrl of pageImageUrls) {
if (!downloadedUrls.has(imageUrl)) {
downloadedUrls.add(imageUrl);
const pinIdMatch = pinUrl.match(/\/pin\/(\d+)\/?/);
const basePinId = pinIdMatch ? pinIdMatch[1] : `image_${Date.now()}`;
const extension = path.extname(new URL(imageUrl).pathname) || '.jpg';
const filename = `${basePinId}_related_${imageCount++}${extension}`;
const filepath = path.join(downloadPath, filename);
logger.debug(`Downloading ${imageUrl} to ${filepath}`);
await downloadImage(imageUrl, filepath);
downloadedImagePaths.push(filepath);
}
}
} catch (error) {
logger.error(`An error occurred while processing pin ${pinUrl}:`, error);
}
await browser.close();
logger.debug('Done processing pin.');
return downloadedImagePaths;
}

504
src/lib/image-converter.ts Normal file
View File

@ -0,0 +1,504 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface ImageSize {
width: number;
height: number;
}
async function convertImage(
prompt: string,
baseFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 },
useEmpltyLatent: boolean = false
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
if (useEmpltyLatent) {
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/edit_image_2_qwen_empty.json', 'utf-8'));
workflow['21']['inputs']['value'] = prompt;
workflow['30']['inputs']['width'] = size.width;
workflow['31']['inputs']['height'] = size.height;
workflow['14']['inputs']['image'] = baseFileName;
} else {
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/edit_image_qwen.json', 'utf-8'));
workflow['21']['inputs']['value'] = prompt;
workflow['23']['inputs']['width'] = size.width;
workflow['23']['inputs']['height'] = size.height;
workflow['14']['inputs']['image'] = baseFileName;
}
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('qwenedit'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', baseFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
// basefilename is connected to image2
// sencondfilename is connect to image1
async function convertImageWithFile(
prompt: string,
baseFileName: string,
secondFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/edit_image_2_qwen.json', 'utf-8'));
workflow['21']['inputs']['value'] = prompt;
workflow['31']['inputs']['Value'] = size.width;
workflow['32']['inputs']['Value'] = size.height;
workflow['14']['inputs']['image'] = baseFileName;
workflow['29']['inputs']['image'] = secondFileName;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('combined'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', baseFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
// basefilename is connected to image1
// sencondfilename is connect to image2
async function convertImageWithFileHandbag(
prompt: string,
baseFileName: string,
secondFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/edit_image_2_qwen_handbag.json', 'utf-8'));
workflow['21']['inputs']['value'] = prompt;
workflow['31']['inputs']['Value'] = size.width;
workflow['32']['inputs']['Value'] = size.height;
workflow['14']['inputs']['image'] = baseFileName;
workflow['29']['inputs']['image'] = secondFileName;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('combined'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', baseFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
// basefilename is connected to image1
// sencondfilename is connect to image2
async function convertImageWithFileForPose(
prompt: string,
baseFileName: string,
secondFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/edit_image_2_qwen_pose.json', 'utf-8'));
workflow['21']['inputs']['value'] = prompt;
workflow['31']['inputs']['Value'] = size.width;
workflow['32']['inputs']['Value'] = size.height;
workflow['14']['inputs']['image'] = baseFileName;
workflow['29']['inputs']['image'] = secondFileName;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('combined'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', baseFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export async function extractCloth(
prompt: string,
baseFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/cloth_extractor.json', 'utf-8'));
workflow['21']['inputs']['value'] = prompt;
workflow['24']['inputs']['width'] = size.width;
workflow['24']['inputs']['height'] = size.height;
workflow['64']['inputs']['image'] = baseFileName;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('qwenedit'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', baseFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export async function convertImageWithMultipleFile(
prompt: string,
srcFiles: string[],
outputFile: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/edit_image_multiple_qwen.json', 'utf-8'));
workflow['21']['inputs']['value'] = prompt;
workflow['76']['inputs']['number'] = size.width;
workflow['77']['inputs']['number'] = size.height;
if (srcFiles[0])
workflow['64']['inputs']['image'] = srcFiles[0];
if (srcFiles[1])
workflow['15']['inputs']['image'] = srcFiles[1];
if (srcFiles[2])
workflow['68']['inputs']['image'] = srcFiles[2];
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('qwenedit'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', outputFile);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export async function convertImageVton(
personFile: string,
clothFile: string,
outputFile: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/vton_cloth.json', 'utf-8'));
workflow['76']['inputs']['number'] = size.width;
workflow['77']['inputs']['number'] = size.height;
workflow['15']['inputs']['image'] = personFile;
workflow['64']['inputs']['image'] = clothFile;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('qwenedit'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', outputFile);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export async function convertImageVtonPose(
personFile: string,
clothFile: string,
poseFile: string,
outputFile: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/vton.json', 'utf-8'));
workflow['76']['inputs']['number'] = size.width;
workflow['77']['inputs']['number'] = size.height;
workflow['15']['inputs']['image'] = personFile;
workflow['64']['inputs']['image'] = clothFile;
workflow['68']['inputs']['image'] = poseFile;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('qwenedit'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', outputFile);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export { convertImage, convertImageWithFile, convertImageWithFileForPose, convertImageWithFileHandbag };

View File

@ -0,0 +1,77 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface ImageSize {
width: number;
height: number;
}
async function generateImage(
prompt: string,
faceImage: string,
newFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/generate_image_with_face.json', 'utf-8'));
workflow['41']['inputs']['clip_l'] = prompt;
workflow['41']['inputs']['t5xxl'] = prompt;
// Set image name
workflow['49']['inputs']['image'] = faceImage;
// Set image name
//workflow['16']['inputs']['image'] = imageName2;
workflow['27']['inputs']['width'] = size.width;
workflow['27']['inputs']['height'] = size.height;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('FACEIMAGE'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export { generateImage };

View File

@ -0,0 +1,78 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface ImageSize {
width: number;
height: number;
}
async function generateImage(
prompt: string,
imageName1: string,
imageName2: string,
newFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/generate_image_mix_style.json', 'utf-8'));
workflow['14']['inputs']['text'] = prompt;
// Set image name
workflow['13']['inputs']['image'] = imageName1;
// Set image name
workflow['16']['inputs']['image'] = imageName2;
workflow['3']['inputs']['width'] = size.width;
workflow['3']['inputs']['height'] = size.height;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('STYLEDVIDEOMAKER'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export { generateImage };

View File

@ -0,0 +1,78 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface ImageSize {
width: number;
height: number;
}
async function generateImage(
prompt: string,
faceImage: string,
styleImage: string,
newFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/generate_image_style_faceswap.json', 'utf-8'));
workflow['14']['inputs']['text'] = prompt;
// Set image name
workflow['13']['inputs']['image'] = styleImage;
// Set image name
workflow['19']['inputs']['image'] = faceImage;
workflow['3']['inputs']['width'] = size.width;
workflow['3']['inputs']['height'] = size.height;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('STYLEDVIDEOMAKER'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export { generateImage };

View File

@ -62,10 +62,15 @@ async function generateImage(
const newFilePath = path.resolve('./generated', newFileName); const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true }); await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile); const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath); await fs.copyFile(sourcePath, newFilePath);
//await fs.unlink(sourcePath);
return newFilePath; return newFilePath;
} }

118
src/lib/image-upscaler.ts Normal file
View File

@ -0,0 +1,118 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface ImageSize {
width: number;
height: number;
}
async function facerestore_upscale(
baseFileName: string,
faceReferenceName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/facerestore_upscale.json', 'utf-8'));
workflow['1']['inputs']['image'] = baseFileName;
workflow['3']['inputs']['image'] = faceReferenceName;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('upscaled'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', baseFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
async function upscale(
baseFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/upscale.json', 'utf-8'));
workflow['111']['inputs']['image'] = baseFileName;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('upscaled'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', baseFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export { facerestore_upscale, upscale };

View File

@ -4,25 +4,27 @@ import { logger } from './logger';
dotenv.config(); dotenv.config();
const LLM_BASE_URL = process.env.LLM_BASE_URL; const LMSTUDIO_BASE_URL = process.env.LMSTUDIO_BASE_URL;
const LMSTUDIO_API_KEY = process.env.LMSTUDIO_API_KEY;
const LMSTUDIO_MODEL = process.env.LMSTUDIO_MODEL;
async function callLMStudio(prompt: string): Promise<any> { async function callLmstudio(prompt: string): Promise<any> {
if (!LLM_BASE_URL) { if (!LMSTUDIO_BASE_URL) {
throw new Error('LLM_BASE_URL is not defined in the .env file'); throw new Error('LMSTUDIO_BASE_URL is not defined in the .env file');
} }
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
let llmResponse = ""; let llmResponse = "";
try { try {
const requestUrl = new URL('v1/chat/completions', LLM_BASE_URL); const response = await fetch(`${LMSTUDIO_BASE_URL}/chat/completions`, {
const response = await fetch(requestUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${LMSTUDIO_API_KEY}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
model: 'local-model', model: LMSTUDIO_MODEL,
messages: [ messages: [
{ {
role: 'user', role: 'user',
@ -40,15 +42,19 @@ async function callLMStudio(prompt: string): Promise<any> {
const jsonMatch = content.match(/\{[\s\S]*\}/); const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) { if (jsonMatch) {
return JSON.parse(jsonMatch[0]); return JSON.parse(jsonMatch[0]);
} else {
const arrayMatch = content.match(/\[[\s\S]*\]/);
if (arrayMatch) {
return JSON.parse(arrayMatch[0]);
}
} }
// If no JSON/array found, return the raw content
return content;
} else { } else {
logger.error('Unexpected API response:', data); logger.error('Unexpected API response:', data);
} }
} catch (error) { } catch (error) {
logger.error(`Attempt ${i + 1} failed:`, error); logger.error(`Attempt ${i + 1} failed:`, error);
if (error instanceof TypeError && error.message.includes('fetch failed')) {
logger.error('Could not connect to the LM Studio server. Please ensure the server is running and accessible at the specified LLM_BASE_URL.');
}
logger.debug(`LLM response: ${llmResponse}`) logger.debug(`LLM response: ${llmResponse}`)
} }
} }
@ -56,9 +62,9 @@ async function callLMStudio(prompt: string): Promise<any> {
throw new Error('Failed to get response from LLM after 10 attempts'); throw new Error('Failed to get response from LLM after 10 attempts');
} }
async function callLMStudioWithFile(imagePath: string, prompt: string): Promise<any> { async function callLMStudioAPIWithFile(imagePath: string, prompt: string): Promise<any> {
if (!LLM_BASE_URL) { if (!LMSTUDIO_BASE_URL) {
throw new Error('LLM_BASE_URL is not defined in the .env file'); throw new Error('LMSTUDIO_BASE_URL is not defined in the .env file');
} }
const imageBuffer = fs.readFileSync(imagePath); const imageBuffer = fs.readFileSync(imagePath);
@ -68,14 +74,14 @@ async function callLMStudioWithFile(imagePath: string, prompt: string): Promise<
let llmResponse = ""; let llmResponse = "";
try { try {
const requestUrl = new URL('v1/chat/completions', LLM_BASE_URL); const response = await fetch(`${LMSTUDIO_BASE_URL}/chat/completions`, {
const response = await fetch(requestUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${LMSTUDIO_API_KEY}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
model: 'local-model', model: LMSTUDIO_MODEL,
messages: [ messages: [
{ {
role: 'user', role: 'user',
@ -96,15 +102,18 @@ async function callLMStudioWithFile(imagePath: string, prompt: string): Promise<
const jsonMatch = content.match(/\{[\s\S]*\}/); const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) { if (jsonMatch) {
return JSON.parse(jsonMatch[0]); return JSON.parse(jsonMatch[0]);
} else {
const arrayMatch = content.match(/\[[\s\S]*\]/);
if (arrayMatch) {
return JSON.parse(arrayMatch[0]);
}
} }
return content;
} else { } else {
logger.error('Unexpected API response:', data); logger.error('Unexpected API response:', data);
} }
} catch (error) { } catch (error) {
logger.error(`Attempt ${i + 1} failed:`, error); logger.error(`Attempt ${i + 1} failed:`, error);
if (error instanceof TypeError && error.message.includes('fetch failed')) {
logger.error('Could not connect to the LM Studio server. Please ensure the server is running and accessible at the specified LLM_BASE_URL.');
}
logger.debug(`LLM response: ${llmResponse}`) logger.debug(`LLM response: ${llmResponse}`)
} }
} }
@ -112,4 +121,4 @@ async function callLMStudioWithFile(imagePath: string, prompt: string): Promise<
throw new Error('Failed to describe image after 10 attempts'); throw new Error('Failed to describe image after 10 attempts');
} }
export { callLMStudio, callLMStudioWithFile }; export { callLmstudio, callLMStudioAPIWithFile };

120
src/lib/openai.ts Normal file
View File

@ -0,0 +1,120 @@
import fs from 'fs';
import dotenv from 'dotenv';
import { logger } from './logger';
dotenv.config();
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const OPENAI_MODEL = process.env.OPENAI_MODEL;
async function callOpenAI(prompt: string): Promise<any> {
if (!OPENAI_API_KEY || !OPENAI_MODEL) {
throw new Error('OPENAI_API_KEY or OPENAI_MODEL is not defined in the .env file');
}
for (let i = 0; i < 10; i++) {
let llmResponse = "";
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: OPENAI_MODEL,
messages: [
{
role: 'user',
content: prompt,
},
],
temperature: 0.7,
}),
});
const data = await response.json();
if (data.choices && data.choices.length > 0) {
const content = data.choices[0].message.content;
llmResponse = content;
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
} else {
const arrayMatch = content.match(/\[[\s\S]*\]/);
if (arrayMatch) {
return JSON.parse(arrayMatch[0]);
}
}
} else {
logger.error('Unexpected API response:', data);
}
} catch (error) {
logger.error(`Attempt ${i + 1} failed:`, error);
logger.debug(`LLM response: ${llmResponse}`)
}
}
throw new Error('Failed to get response from LLM after 10 attempts');
}
async function callOpenAIWithFile(imagePath: string, prompt: string): Promise<any> {
if (!OPENAI_API_KEY || !OPENAI_MODEL) {
throw new Error('OPENAI_API_KEY or OPENAI_MODEL is not defined in the .env file');
}
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
for (let i = 0; i < 10; i++) {
let llmResponse = "";
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: OPENAI_MODEL,
messages: [
{
role: 'user',
content: [
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Image}` } },
{ type: 'text', text: prompt },
],
},
],
temperature: 0.7,
}),
});
const data = await response.json();
if (data.choices && data.choices.length > 0) {
const content = data.choices[0].message.content;
llmResponse = content;
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
} else {
const arrayMatch = content.match(/\[[\s\S]*\]/);
if (arrayMatch) {
return JSON.parse(arrayMatch[0]);
}
}
} else {
logger.error('Unexpected API response:', data);
}
} catch (error) {
logger.error(`Attempt ${i + 1} failed:`, error);
logger.debug(`LLM response: ${llmResponse}`)
}
}
throw new Error('Failed to describe image after 10 attempts');
}
export { callOpenAI, callOpenAIWithFile };

191
src/lib/pinterest.ts Normal file
View File

@ -0,0 +1,191 @@
import { logger } from './logger';
import * as fs from 'fs/promises';
import dotenv from 'dotenv';
import path from 'path';
import puppeteer from 'puppeteer';
export async function getPinUrlFromPinterest(keyword: string): Promise<string | null> {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
const searchUrl = `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(keyword)}`;
await page.goto(searchUrl, { waitUntil: 'networkidle2' });
const scrollCount = Math.floor(Math.random() * 5) + 1;
logger.info(`Scrolling ${scrollCount} times...`);
for (let i = 0; i < scrollCount; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 1000));
}
const pinLinks = await page.$$eval('a', (anchors) =>
anchors.map((a) => a.href).filter((href) => href.includes('/pin/'))
);
if (pinLinks.length > 0) {
return pinLinks[Math.floor(Math.random() * pinLinks.length)];
}
return null;
} catch (error) {
logger.error('Error while getting pin URL from Pinterest:', error);
return null;
} finally {
await browser.close();
}
}
export async function downloadImagesFromPinterestSearch(keyword: string, count: number): Promise<string[]> {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
const searchUrl = `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(keyword)}`;
await page.goto(searchUrl, { waitUntil: 'networkidle2' });
logger.info(`Scrolling 3 times...`);
for (let i = 0; i < 3; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 1000));
}
const imageUrls = await page.$$eval('img', (imgs) => {
const urls: string[] = imgs.map(img => {
const srcset = img.getAttribute('srcset') || '';
if (!srcset) return '';
const parts = srcset.split(',').map(p => p.trim());
for (const part of parts) {
const m = part.match(/^(\S+)\s+4x$/);
if (m && m[1]) return m[1];
}
const src = img.src || '';
if (src.includes('/originals/')) return src;
return '';
}).filter(s => !!s && s.includes('pinimg'));
// Remove duplicates
return [...new Set(urls)];
});
if (imageUrls.length === 0) {
logger.warn(`No 4x image URLs found for keyword "${keyword}"`);
return [];
}
// shuffle and pick up to `count` unique images
const shuffled = imageUrls.slice().sort(() => 0.5 - Math.random());
const chosen = shuffled.slice(0, Math.min(count, shuffled.length));
const outDir = path.join(process.cwd(), 'download');
await fs.mkdir(outDir, { recursive: true });
const results: string[] = [];
for (let i = 0; i < chosen.length; i++) {
const src = chosen[i];
try {
const imgPage = await browser.newPage();
const resp = await imgPage.goto(src, { timeout: 30000, waitUntil: 'networkidle2' });
if (!resp) {
logger.warn(`Failed to fetch image ${src}`);
await imgPage.close();
continue;
}
const buffer = await resp.buffer();
const timestamp = Date.now();
const outPath = path.join(outDir, `${keyword.replace(/\s+/g, '_')}_${timestamp}_${i}.png`);
await fs.writeFile(outPath, buffer);
results.push(outPath);
await imgPage.close();
} catch (err) {
logger.error(`Failed to download image ${src}:`, err);
}
}
return results;
} catch (error) {
logger.error(`Error while downloading images for keyword "${keyword}":`, error);
return [];
} finally {
await browser.close();
}
}
// Download up to `count` images from a pin URL by opening the pin page and scro lling up to 5 times to trigger lazy loading
// Returns an array of saved image paths (may be empty)
export async function downloadImageFromPin(pinUrl: string, count: number = 1): Promise<string[]> {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
await page.goto(pinUrl, { waitUntil: 'networkidle2', timeout: 30000 });
for (let i = 0; i < 3; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise((r) => setTimeout(r, 700 + Math.random() * 800));
}
const imgs: string[] = await page.$$eval('img', imgs => {
// For each <img> try to extract the 4x (original) URL from srcset.
// srcset example:
// "https://i.pinimg.com/236x/...jpg 1x, https://i.pinimg.com/474x/...jpg 2x, https://i.pinimg.com/736x/...jpg 3x, https://i.pinimg.com/originals/...jpg 4x"
const urls: string[] = imgs.map(img => {
const srcset = (img as HTMLImageElement).getAttribute('srcset') || '';
if (!srcset) return '';
const parts = srcset.split(',').map(p => p.trim());
for (const part of parts) {
const m = part.match(/^(\S+)\s+4x$/);
if (m && m[1]) return m[1];
}
// fallback: if src contains "originals" return src
const src = (img as HTMLImageElement).src || '';
if (src.includes('/originals/')) return src;
return '';
}).filter(s => !!s && s.includes('pinimg'));
return urls;
});
if (!imgs || imgs.length === 0) {
logger.warn(`No image src (4x) found on pin page ${pinUrl}`);
return [];
}
// shuffle and pick up to `count` unique images
const shuffled = imgs.slice().sort(() => 0.5 - Math.random());
const chosen = shuffled.slice(0, Math.min(count, shuffled.length));
const outDir = path.join(process.cwd(), 'download');
await fs.mkdir(outDir, { recursive: true });
const results: string[] = [];
for (let i = 0; i < chosen.length; i++) {
const src = chosen[i];
try {
const imgPage = await browser.newPage();
const resp = await imgPage.goto(src, { timeout: 30000, waitUntil: 'networkidle2' });
if (!resp) {
logger.warn(`Failed to fetch image ${src} from ${pinUrl}`);
await imgPage.close();
continue;
}
const buffer = await resp.buffer();
const pinId = pinUrl.split('/').filter(Boolean).pop() || `pin_${Date.now()}`;
const timestamp = Date.now();
const outPath = path.join(outDir, `${pinId}_${timestamp}_${i}.png`);
await fs.writeFile(outPath, buffer);
results.push(outPath);
await imgPage.close();
} catch (err) {
logger.error(`Failed to download image ${src} from ${pinUrl}:`, err);
}
}
return results;
} catch (err) {
logger.error(`Failed to download images from ${pinUrl}:`, err);
return [];
} finally {
await browser.close();
}
}

67
src/lib/util.ts Normal file
View File

@ -0,0 +1,67 @@
// png-json-metadata.ts
import * as fs from "fs";
import extract from "png-chunks-extract";
import encodeChunks from "png-chunks-encode";
import * as textChunk from "png-chunk-text";
type PngChunk = { name: string; data: Uint8Array };
/**
* PNG へ JSON を Base64 で埋め込むtEXt / keyword: "json-b64"
* - JSON は UTF-8 → Base64 にして ASCII 化tEXt の Latin-1 制限を回避)
* - 既存の "json-b64" tEXt があれば置き換え(重複回避)
*/
export async function embedJsonToPng(path: string, obj: unknown): Promise<void> {
const input = fs.readFileSync(path);
const chunks = extract(input) as PngChunk[];
// 既存の "json-b64" tEXt を除外
const filtered: PngChunk[] = chunks.filter((c) => {
if (c.name !== "tEXt") return true;
try {
const decoded = textChunk.decode(c.data); // { keyword, text }
return decoded.keyword !== "json-b64";
} catch {
// decode 失敗(別の形式など)は残す
return true;
}
});
const json = JSON.stringify(obj);
const b64 = Buffer.from(json, "utf8").toString("base64"); // ASCII のみ
// encode() は { name:'tEXt', data: Uint8Array } を返す
const newChunk = textChunk.encode("json-b64", b64) as PngChunk;
// IEND の直前に挿入PNG の正しい順序を維持)
const iendIndex = filtered.findIndex((c) => c.name === "IEND");
if (iendIndex < 0) {
throw new Error("Invalid PNG: missing IEND chunk.");
}
filtered.splice(iendIndex, 0, newChunk);
const out = Buffer.from(encodeChunks(filtered));
fs.writeFileSync(path, out);
}
/**
* PNG から Base64 JSONtEXt / keyword: "json-b64")を読み出す
*/
export async function readJsonToPng(path: string): Promise<any> {
const input = fs.readFileSync(path);
const chunks = extract(input) as PngChunk[];
for (const c of chunks) {
if (c.name !== "tEXt") continue;
try {
const { keyword, text } = textChunk.decode(c.data);
if (keyword === "json-b64") {
const json = Buffer.from(text, "base64").toString("utf8");
return JSON.parse(json);
}
} catch {
// 他の tEXt / 壊れたエントリは無視
}
}
throw new Error("No base64 JSON found in PNG (tEXt keyword 'json-b64').");
}

View File

@ -0,0 +1,93 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface VideoSize {
width: number;
height: number;
}
async function generateStyledVideo(
imagePrompt: string,
videoPrompt: string,
imageName: string,
newFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: VideoSize = { width: 320, height: 640 }
): Promise<{ videoPath: string, imagePath: string }> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
const workflow = JSON.parse(await fs.readFile('src/comfyworkflows/prototyping_style_flux_wan22.json', 'utf-8'));
// Set prompts
workflow['95']['inputs']['text'] = imagePrompt;
workflow['105']['inputs']['text'] = videoPrompt;
// Set image name
workflow['114']['inputs']['image'] = imageName;
// Set sizes
workflow['104']['inputs']['width'] = size.width;
workflow['104']['inputs']['height'] = size.height;
workflow['97']['inputs']['width'] = size.width;
workflow['97']['inputs']['height'] = size.height;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
// Find the latest MP4 file
const generatedVideos = files.filter(file => file.endsWith('.mp4'));
const videoStats = await Promise.all(
generatedVideos.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
videoStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestVideo = videoStats[0].file;
// Find the latest PNG file
const generatedImages = files.filter(file => file.endsWith('.png'));
const imageStats = await Promise.all(
generatedImages.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
imageStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestImage = imageStats[0].file;
// Create new file paths
const newVideoPath = path.resolve('./generated', newFileName);
const newImagePath = path.resolve('./generated', newFileName.replace('.mp4', '.png'));
await fs.mkdir('./generated', { recursive: true });
// Copy both files
const sourceVideoPath = path.join(COMFY_OUTPUT_DIR!, latestVideo);
await fs.copyFile(sourceVideoPath, newVideoPath);
const sourceImagePath = path.join(COMFY_OUTPUT_DIR!, latestImage);
await fs.copyFile(sourceImagePath, newImagePath);
// Optionally, unlink the source files
// await fs.unlink(sourceVideoPath);
// await fs.unlink(sourceImagePath);
return { videoPath: newVideoPath, imagePath: newImagePath }
}
export { generateStyledVideo };

View File

@ -0,0 +1,104 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface VideoSize {
width: number;
height: number;
}
async function generateVideo(
prompt: string,
imagePath: string,
newFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: VideoSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
const workflow = JSON.parse(await fs.readFile('src/comfyworkflows/generate_video_text.json', 'utf-8'));
workflow['27']['inputs']['text'] = prompt;
workflow['28']['inputs']['width'] = size.width;
workflow['28']['inputs']['height'] = size.height;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
await new Promise(resolve => setTimeout(resolve, 1000));
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
// find latest .mp4 file (video) in the comfy output dir
const generatedFiles = files.filter(file => file.endsWith('.mp4'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
if (fileStats.length === 0) {
throw new Error('No generated mp4 files found in comfy output directory.');
}
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
await fs.copyFile(sourcePath, newFilePath);
// Handle the static image (.png)
// Expected image name: same base name as the video, but .png extension
const expectedImageName = path.basename(newFileName, path.extname(newFileName)) + '.png';
const pngFiles = files.filter(file => file.endsWith('.png'));
let sourcePngFile: string | null = null;
// Prefer exact match (Comfy sometimes names the image exactly as the video base name)
if (pngFiles.includes(expectedImageName)) {
sourcePngFile = expectedImageName;
} else if (pngFiles.length > 0) {
// Fallback: pick the most recent .png by timestamp
const pngStats = await Promise.all(
pngFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
pngStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
sourcePngFile = pngStats[0].file;
}
if (sourcePngFile) {
const targetPngPath = path.resolve('./generated', expectedImageName);
// Delete existing target image if present
try {
await fs.unlink(targetPngPath);
} catch (err) {
// ignore if not exists
}
const sourcePngPath = path.join(COMFY_OUTPUT_DIR!, sourcePngFile);
// Copy and rename the png to the generated folder with the expected name
await fs.copyFile(sourcePngPath, targetPngPath);
}
return newFilePath;
}
export { generateVideo };

View File

@ -16,16 +16,22 @@ async function generateVideo(
newFileName: string, newFileName: string,
comfyBaseUrl: string, comfyBaseUrl: string,
comfyOutputDir: string, comfyOutputDir: string,
size: VideoSize = { width: 720, height: 1280 } size: VideoSize = { width: 720, height: 1280 },
isShort = false,
isLight = false
): Promise<string> { ): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, ''); const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir; const COMFY_OUTPUT_DIR = comfyOutputDir;
const workflow = JSON.parse(await fs.readFile('src/comfyworkflows/generate_video.json', 'utf-8')); const workflow = isLight ?
JSON.parse(await fs.readFile('src/comfyworkflows/generate_video_light.json', 'utf-8')) :
JSON.parse(await fs.readFile('src/comfyworkflows/generate_video.json', 'utf-8'));
workflow['6']['inputs']['text'] = prompt; workflow['6']['inputs']['text'] = prompt;
workflow['52']['inputs']['image'] = imagePath; workflow['52']['inputs']['image'] = imagePath;
workflow['64']['inputs']['width'] = size.width; workflow['64']['inputs']['width'] = size.width;
workflow['64']['inputs']['height'] = size.height; workflow['64']['inputs']['height'] = size.height;
if (isShort) workflow['50']['inputs']['length'] = 89;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow }); const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id; const promptId = response.data.prompt_id;
@ -52,7 +58,7 @@ async function generateVideo(
const newFilePath = path.resolve('./generated', newFileName); const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true }); await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile); const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
await fs.copyFile(sourcePath, newFilePath); await fs.copyFile(sourcePath, newFilePath);
//await fs.unlink(sourcePath); //await fs.unlink(sourcePath);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,598 @@
{
"song": {
"title": "Četrdesete: Svjetlo kroz Maglu",
"artist": "Radni Front",
"genre": "Cinematic Pop/Rock",
"mood": "Gritty, uplifting, hopeful"
},
"character": {
"bodyType": "average build, slightly athletic, in his 40s",
"hairStyle": "short side-part, light stubble",
"accessories": "leather messenger bag, analog watch, simple ring"
},
"scenes": [
{
"sceneId": 1,
"time": "Morning",
"location": "Foggy city street at dawn",
"outfit": "charcoal wool coat over navy blazer, white oxford shirt, dark chinos, leather derbies, grey scarf; takeaway coffee in hand",
"cuts": [
{
"cutId": 1,
"pose": "walking briskly with coffee",
"action": "exhaling visible breath in cold air",
"camera": [
"full-body frontal walk through light shafts in fog",
"low angle on shoes splashing a thin puddle"
]
},
{
"cutId": 2,
"pose": "pausing at crosswalk",
"action": "checking watch then stepping forward",
"camera": [
"close-up on watch face catching sun flare",
"three-quarter side track across zebra lines"
]
},
{
"cutId": 3,
"pose": "tight grip on cup",
"action": "lifting cup for a sip",
"camera": [
"macro fingers on cup sleeve with steam",
"profile head-and-shoulders backlit by hazy sun"
]
},
{
"cutId": 4,
"pose": "steady stride",
"action": "adjusting scarf in breeze",
"camera": [
"telephoto compression down foggy avenue",
"rear follow shot with scarf rim-lit"
]
},
{
"cutId": 5,
"pose": "determined gaze",
"action": "looking ahead into sunlight",
"camera": [
"tight face with soft lens bloom",
"over-shoulder toward blinding sky gap"
]
}
]
},
{
"sceneId": 2,
"time": "Morning",
"location": "Tram interior rocking through city",
"outfit": "navy blazer, knit crewneck, white shirt, dark jeans, leather sneakers; coffee in hand",
"cuts": [
{
"cutId": 1,
"pose": "standing in tram",
"action": "holding strap with one hand, coffee in the other",
"camera": [
"medium inside tram, window condensation streaks",
"front angle from aisle with passing light bands"
]
},
{
"cutId": 2,
"pose": "leaning against pole",
"action": "checking phone briefly",
"camera": [
"over-shoulder to phone screen glow",
"side profile with rim light from window"
]
},
{
"cutId": 3,
"pose": "looking out window",
"action": "soft smile seeing morning city",
"camera": [
"through-glass face framed by droplets",
"exterior-to-interior angle catching reflection layers"
]
},
{
"cutId": 4,
"pose": "steady stance",
"action": "taking small sip as tram turns",
"camera": [
"low angle on cup tilt and sleeve",
"rear quarter shot showing sway of carriage"
]
},
{
"cutId": 5,
"pose": "composed",
"action": "pocketing phone, focusing forward",
"camera": [
"chest-up push-in down the aisle",
"top-down seat pattern with soft bloom"
]
}
]
},
{
"sceneId": 3,
"time": "Morning",
"location": "Modern office desk with soft skylight",
"outfit": "light grey blazer, pale blue shirt, dark chinos, brown brogues; ID lanyard",
"cuts": [
{
"cutId": 1,
"pose": "seated typing",
"action": "focused work with subtle nods",
"camera": [
"medium desk-side angle with window beams",
"close-up fingers on keyboard with keycap sheen"
]
},
{
"cutId": 2,
"pose": "reviewing papers",
"action": "marking with pen",
"camera": [
"top-down documents in stripey sunlight",
"front low angle across desk edge"
]
},
{
"cutId": 3,
"pose": "phone to ear",
"action": "short professional conversation",
"camera": [
"profile with rim-lit jawline",
"over-shoulder to city glow outside"
]
},
{
"cutId": 4,
"pose": "stretching shoulders",
"action": "exhaling, sip of water",
"camera": [
"medium with glass prisming light",
"close-up on water surface ripples"
]
},
{
"cutId": 5,
"pose": "refocused",
"action": "resuming typing with resolve",
"camera": [
"front push-in past monitor edge",
"rear silhouette against bright window"
]
}
]
},
{
"sceneId": 4,
"time": "Late Morning",
"location": "Office glass meeting room",
"outfit": "navy sport coat, white shirt, knit tie, tailored trousers",
"cuts": [
{
"cutId": 1,
"pose": "standing at screen",
"action": "pointing to chart, calm tone",
"camera": [
"three-quarter presenter through glass reflections",
"over-shoulder to slide glow on faces"
]
},
{
"cutId": 2,
"pose": "listening",
"action": "nodding, hands clasped",
"camera": [
"medium frontal with rim edge light",
"tight hands interlaced catching sheen"
]
},
{
"cutId": 3,
"pose": "whiteboard note",
"action": "writing key word",
"camera": [
"side on marker tip against board",
"over-shoulder on word under skylight stripe"
]
},
{
"cutId": 4,
"pose": "hand gestures",
"action": "framing idea in air",
"camera": [
"front medium with floating dust motes",
"profile gesture silhouette against window"
]
},
{
"cutId": 5,
"pose": "closing remark",
"action": "small confident smile",
"camera": [
"tight smile with gentle bloom",
"wide room applauding in soft backlight"
]
}
]
},
{
"sceneId": 5,
"time": "Midday",
"location": "Quiet park bench under soft sun",
"outfit": "olive field jacket over white shirt, navy chinos, sneakers",
"cuts": [
{
"cutId": 1,
"pose": "seated on bench",
"action": "unwrapping sandwich",
"camera": [
"wide bench under dappled leaves",
"close-up foil crackle catching sparkles"
]
},
{
"cutId": 2,
"pose": "bite and look up",
"action": "watching birds",
"camera": [
"tight mouthful with flare peeking",
"low up-angle to glowing canopy"
]
},
{
"cutId": 3,
"pose": "relaxed lean",
"action": "closing eyes briefly",
"camera": [
"front push-in eyes closed in sun warmth",
"overhead bench and soft shadow lace"
]
},
{
"cutId": 4,
"pose": "checking phone",
"action": "soft smile at message",
"camera": [
"over-shoulder to screen in shade",
"side profile with sun peeking lens flare"
]
},
{
"cutId": 5,
"pose": "standing stretch",
"action": "deep breath hands on hips",
"camera": [
"rear sun halo and long shadow",
"front medium with leaf bokeh confetti"
]
}
]
},
{
"sceneId": 6,
"time": "Afternoon",
"location": "Open office collaboration area",
"outfit": "rolled-sleeve shirt, blazer off, vest optional, dark trousers",
"cuts": [
{
"cutId": 1,
"pose": "standing with teammates",
"action": "high-five and laugh",
"camera": [
"wide group under skylight glow",
"tight palms meeting with light burst"
]
},
{
"cutId": 2,
"pose": "pointing at laptop",
"action": "explaining step-by-step",
"camera": [
"over-shoulder to UI reflecting in eyes",
"front trio with bright window beams"
]
},
{
"cutId": 3,
"pose": "shared joke",
"action": "covering mouth laughing",
"camera": [
"tight eyes creasing with sparkle",
"side candid smiles with lens bloom"
]
},
{
"cutId": 4,
"pose": "sticky note placement",
"action": "slapping idea on board",
"camera": [
"macro note corner catching shine",
"front push-in to colorful board grid"
]
},
{
"cutId": 5,
"pose": "wrap-up clap",
"action": "small bow to team",
"camera": [
"medium applause with soft halos",
"rear shoulders rim-lit against corridor"
]
}
]
},
{
"sceneId": 7,
"time": "Evening",
"location": "Recording studio vocal booth",
"outfit": "black henley under dark blazer, dark jeans; studio headphones, pop filter",
"cuts": [
{
"cutId": 1,
"pose": "standing at mic",
"action": "closing eyes and singing",
"camera": [
"medium mic and pop filter with tube glow",
"profile head tilt with headphone sheen"
]
},
{
"cutId": 2,
"pose": "one hand on headphone",
"action": "finding pitch",
"camera": [
"tight hand on ear cup",
"over-shoulder to waveform screen in control room"
]
},
{
"cutId": 3,
"pose": "leaning toward mic",
"action": "delivering powerful line",
"camera": [
"front intense eyes past grille",
"low angle along mic stand to face"
]
},
{
"cutId": 4,
"pose": "step back",
"action": "nod to beat, smile",
"camera": [
"wide booth with warm bokeh bulbs",
"tight smile reflected in glass"
]
},
{
"cutId": 5,
"pose": "hands open",
"action": "finishing phrase softly",
"camera": [
"front soft flare across frame",
"overhead hands lowering into light cone"
]
}
]
},
{
"sceneId": 8,
"time": "Golden Hour",
"location": "City park path with family",
"outfit": "sand blazer, light knit, dark denim; family in cozy casual",
"cuts": [
{
"cutId": 1,
"pose": "holding partners hand",
"action": "walking with child skipping ahead",
"camera": [
"rear wide with low sun flare",
"front medium through tree bokeh"
]
},
{
"cutId": 2,
"pose": "lifting child",
"action": "spinning gently",
"camera": [
"front slow shutter light trails around",
"top-down arms forming circle with sun rim"
]
},
{
"cutId": 3,
"pose": "group hug",
"action": "closing eyes contentedly",
"camera": [
"tight embrace hands with ring glint",
"side cheek-to-hair halo"
]
},
{
"cutId": 4,
"pose": "pointing to sky",
"action": "showing plane to child",
"camera": [
"up-angle to contrail through warm haze",
"front medium faces reflecting sky"
]
},
{
"cutId": 5,
"pose": "hands intertwined",
"action": "walking toward camera",
"camera": [
"front slow push with golden confetti bokeh",
"low angle on joined hands swinging"
]
}
]
},
{
"sceneId": 9,
"time": "Night",
"location": "City viewpoint with beautiful skyline",
"outfit": "charcoal overcoat, black turtleneck, tailored trousers",
"cuts": [
{
"cutId": 1,
"pose": "leaning on railing",
"action": "taking in skyline",
"camera": [
"wide skyline as glittering backdrop",
"profile rim light from city glow"
]
},
{
"cutId": 2,
"pose": "deep breath",
"action": "exhale mist into night",
"camera": [
"tight breath vapor lit by neon",
"rear silhouette against bokeh lights"
]
},
{
"cutId": 3,
"pose": "phone photo",
"action": "capturing skyline",
"camera": [
"over-shoulder to bright screen framing towers",
"low angle to phone and light crown above"
]
},
{
"cutId": 4,
"pose": "hands in pockets",
"action": "swaying to distant music",
"camera": [
"medium chest-up with drifting car streaks",
"tight shoe tapping in puddle reflection"
]
},
{
"cutId": 5,
"pose": "confident gaze",
"action": "looking past camera",
"camera": [
"front tight eyes with city sparkles",
"top-down platform grid glimmering"
]
}
]
},
{
"sceneId": 10,
"time": "Night",
"location": "Office district streets after hours",
"outfit": "dark blazer, open collar shirt, slim trousers, chelsea boots",
"cuts": [
{
"cutId": 1,
"pose": "walking alone",
"action": "hands in pockets",
"camera": [
"long-lens frontal with pearl-like streetlights",
"rear follow on wet pavement reflections"
]
},
{
"cutId": 2,
"pose": "pause under lamp",
"action": "looking up between towers",
"camera": [
"upward angle of converging buildings and lamp star",
"side silhouette against lobby glow"
]
},
{
"cutId": 3,
"pose": "check messages",
"action": "small nod of relief",
"camera": [
"over-shoulder phone light on face",
"handheld parallax of glass light columns"
]
},
{
"cutId": 4,
"pose": "crossing street",
"action": "steady stride",
"camera": [
"wide zebra stripes with mirror reflections",
"low angle heel strike and passing headlight streak"
]
},
{
"cutId": 5,
"pose": "brief smile",
"action": "exhale, continue walking",
"camera": [
"tight smile with neon glimmer",
"front slow push through flare streaks"
]
}
]
},
{
"sceneId": 11,
"time": "Night",
"location": "Home living room, warm lamps",
"outfit": "soft cardigan over tee, lounge chinos, socks",
"cuts": [
{
"cutId": 1,
"pose": "sinking into sofa",
"action": "deep relieved breath",
"camera": [
"medium cozy frame with lamp bloom",
"low angle to floor lamp starburst"
]
},
{
"cutId": 2,
"pose": "family photo glance",
"action": "gentle smile at frame",
"camera": [
"close-up photo glass catching sparkle",
"side hand brushing frame with glint"
]
},
{
"cutId": 3,
"pose": "stretch and yawn",
"action": "neck roll, relaxed shoulders",
"camera": [
"front medium as light wraps softly",
"rear silhouette against curtained window halo"
]
},
{
"cutId": 4,
"pose": "notebook moment",
"action": "jotting tomorrows plan",
"camera": [
"top-down page and pen tip glint",
"three-quarter profile calm focus"
]
},
{
"cutId": 5,
"pose": "closing lights",
"action": "switching lamp off",
"camera": [
"tight finger on switch—tiny spark",
"wide room dimming to city twinkle outside"
]
}
]
}
]
}

View File

@ -0,0 +1,68 @@
{
"song": {
"title": "Četrdesete: Svjetlo kroz Maglu",
"artist": "Radni Front",
"genre": "Cinematic Pop/Rock",
"mood": "Gritty, uplifting, hopeful"
},
"character": {
"bodyType": "average build, slightly athletic, in his 40s",
"hairStyle": "short side-part, light stubble",
"accessories": "leather messenger bag, analog watch, simple ring"
},
"scenes": [
{
"sceneId": 1,
"time": "Morning",
"location": "Foggy city street at dawn",
"outfit": "down jacket, scarf, jeans, boots, t-shirt, hiphop style",
"cuts": [
{
"cutId": 1,
"pose": "walking ",
"action": "exhaling visible breath in cold air",
"camera": [
"full-body frontal walk through light shafts in fog",
"low angle on shoes splashing a thin puddle"
]
},
{
"cutId": 2,
"pose": "walking hands in pockets",
"action": "",
"camera": [
"close-up on watch face catching sun flare",
"three-quarter side track across zebra lines"
]
},
{
"cutId": 3,
"pose": "standing hands in pockets",
"action": "",
"camera": [
"macro fingers on cup sleeve with steam",
"profile head-and-shoulders backlit by hazy sun"
]
},
{
"cutId": 4,
"pose": "steady stride",
"action": "adjusting scarf in breeze",
"camera": [
"telephoto compression down foggy avenue",
"rear follow shot with scarf rim-lit"
]
},
{
"cutId": 5,
"pose": "determined gaze",
"action": "looking ahead into sunlight",
"camera": [
"tight face with soft lens bloom",
"over-shoulder toward blinding sky gap"
]
}
]
}
]
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,786 @@
{
"song": {
"title": "Svjetlo kroz Maglu",
"artist": "Radni Dan",
"genre": "Pop Rock / Motivational Rap",
"mood": "Hopeful, gritty, determined"
},
"character": {
"bodyType": "average 40s male, sturdy build",
"hairStyle": "short neat hair with slight side part, light stubble",
"outfitPalette": "navy, charcoal, camel, white, subtle textures",
"defaultOutfit": "smart-casual: tailored blazer, oxford shirt (top button open), slim chinos, leather sneakers or derby shoes, minimalist watch, messenger bag"
},
"scenes": [
{
"sceneId": 1,
"lyricAnchor": "intro",
"time": "Early Morning",
"location": "Foggy street in a quiet city block",
"outfit": "camel coat over navy blazer and white oxford, charcoal chinos, leather sneakers; steaming paper coffee cup in hand",
"lightFX": "soft ground fog, cool blue ambience, warm shop-window spill, rays through mist",
"cuts": [
{
"cutId": 1,
"pose": "walking steadily with coffee, shoulders relaxed",
"action": "breath visible in cold air",
"camera": [
"full body tracking through fog with distant headlights bokeh",
"side shot catching coffee steam in backlight",
"face zoom in with gentle lens flare from streetlamp",
"overhead drone-lite glide showing puddle reflections",
"back shot with long god rays slicing mist"
]
},
{
"cutId": 2,
"pose": "pauses at crosswalk",
"action": "checks watch, sips coffee",
"camera": [
"low angle shoe splash past a glowing puddle",
"medium shot watch face catching specular highlight",
"face zoom in through drifting fog",
"back shot red-to-green signal glow on mist",
"overhead crosswalk stripes fading into haze"
]
},
{
"cutId": 3,
"pose": "resumes walk",
"action": "adjusts strap of messenger bag",
"camera": [
"tracking three-quarter with storefront light streaks",
"side shot shallow DOF light orbs (bokeh)",
"face zoom in calm focus",
"reflected view in wet window pane",
"wide establishing with soft sunrise rim light"
]
},
{
"cutId": 4,
"pose": "stops to let bicycle pass",
"action": "nods politely",
"camera": [
"over-shoulder bicycle headlights streaking",
"medium profile with fog swirls lit from back",
"ground-level reflection of both in puddle",
"wide silhouette against pale dawn sky",
"face close-up catching tiny snowlike mist droplets"
]
},
{
"cutId": 5,
"pose": "steps onto tram platform edge",
"action": "exhales, looks down the track",
"camera": [
"back shot rails vanishing into fog",
"side shot coffee steam crossing light beam",
"face zoom in hopeful gaze",
"overhead platform LEDs forming dotted trail",
"low angle rails with subtle lens flare"
]
}
]
},
{
"sceneId": 2,
"lyricAnchor": "verse A",
"time": "Morning",
"location": "City tram interior, gently swaying",
"outfit": "navy blazer, white oxford, grey chinos; coffee cup; transit card",
"lightFX": "cool daylight through foggy windows, window streak glares, rhythmic flicker of tunnels",
"cuts": [
{
"cutId": 1,
"pose": "standing near door, one hand on pole",
"action": "sips coffee, eyes on passing streets",
"camera": [
"full body with window streaks bokeh outside",
"side shot with parallax of city blur",
"face zoom in catching soft rim light",
"reflection shot in tram door glass",
"overhead passengers silhouettes in soft haze"
]
},
{
"cutId": 2,
"pose": "leans on pole",
"action": "checks phone briefly (calendar alert)",
"camera": [
"close on phone UI glow reflecting on face",
"low angle hand + coffee, light streaks above",
"medium shot over-shoulder to window",
"face zoom in steadied breathing",
"wide carriage shot with light pulses"
]
},
{
"cutId": 3,
"pose": "shifts stance with sway",
"action": "tightens blazer button",
"camera": [
"profile tightening button with specular highlight",
"back shot rows of seats in perspective",
"face zoom in determined eyes",
"window reflection double exposure feel",
"handheld subtle sway for realism"
]
},
{
"cutId": 4,
"pose": "glances at a child smiling",
"action": "returns a soft smile",
"camera": [
"medium two-shot with warm sun beam",
"face zoom in softened expression",
"side shot catching dust motes in light",
"overhead strap lights forming leading line",
"wide interior with gentle motion blur"
]
},
{
"cutId": 5,
"pose": "tram slows",
"action": "prepares to step out",
"camera": [
"close on shoes as doors open, light washes floor",
"back shot doors parting with faint lens flare",
"face three-quarter with morning glow",
"low angle step-down with steam from cup",
"wide exterior as he exits into bright haze"
]
}
]
},
{
"sceneId": 3,
"lyricAnchor": "verse A",
"time": "Late Morning",
"location": "Modern office, clean lines, window light",
"outfit": "blazer off on chair, rolled sleeves, oxford + chinos; ID badge",
"lightFX": "soft skylight, bounce from white desks, screen glow highlights",
"cuts": [
{
"cutId": 1,
"pose": "seated typing",
"action": "focused, steady pace",
"camera": [
"over-shoulder code/docs on monitor with gentle bloom",
"side shot keypress rhythm, wristwatch glint",
"face zoom in concentration with catchlight",
"overhead tidy desk grid, coffee ring",
"wide office depth with sunbeams"
]
},
{
"cutId": 2,
"pose": "stands to stretch",
"action": "rolls shoulders, exhales",
"camera": [
"medium profile with light streak on edges",
"back shot window rim light tracing silhouette",
"face close slight smile of relief",
"low angle chair wheels reflecting light",
"wide airy office, dust motes floating"
]
},
{
"cutId": 3,
"pose": "points to sticky notes",
"action": "organizes tasks",
"camera": [
"macro sticky notes with pen glide",
"side shot hand gestures casting shadows",
"face zoom in purposeful look",
"overhead desk layout symmetry",
"rack focus foreground notes → him"
]
},
{
"cutId": 4,
"pose": "quick call with headset",
"action": "nods, types one-handed",
"camera": [
"face close with soft headset LED glow",
"side keyboard shot light skimming keys",
"back shot monitor reflections",
"low angle desk edge lens flare",
"wide office with colleagues as bokeh"
]
},
{
"cutId": 5,
"pose": "final keystroke & save",
"action": "small fist pump",
"camera": [
"face zoom in subtle satisfaction",
"overhead enter-key press highlight",
"side shot progress bar completing",
"back shot sun patch moving across floor",
"wide with gentle parallax slide"
]
}
]
},
{
"sceneId": 4,
"lyricAnchor": "bridge",
"time": "Noon",
"location": "Office meeting room with glass walls",
"outfit": "puts blazer back on, pocket square minimal",
"lightFX": "top light soft panels, glass reflections, whiteboard glow",
"cuts": [
{
"cutId": 1,
"pose": "standing by screen",
"action": "presenting calmly",
"camera": [
"three-quarter presenter with screen bloom",
"side shot hand outlining charts",
"face zoom in composed tone",
"overhead table symmetry + notebooks",
"glass reflection two-layer composition"
]
},
{
"cutId": 2,
"pose": "listening to feedback",
"action": "nods, writes key points",
"camera": [
"close pen tip gliding with light spark",
"profile with colleagues blurred",
"face zoom in attentive eyes",
"back shot city light filtering",
"low angle chair legs + floor sheen"
]
},
{
"cutId": 3,
"pose": "whiteboard stand",
"action": "draws simple plan",
"camera": [
"over-shoulder marker stroke with squeak",
"side shot board light wash",
"macro marker cap click",
"face zoom in confident smile",
"wide room with glass reflections"
]
},
{
"cutId": 4,
"pose": "wraps up",
"action": "thanks team with small bow",
"camera": [
"medium group claps, light bloom",
"back shot handshake silhouette",
"face close gratitude",
"overhead chairs and cables geometric",
"wide corridor outside glowing"
]
},
{
"cutId": 5,
"pose": "exits meeting room",
"action": "deep breath reset",
"camera": [
"hallway tracking with specular highlights",
"side shot blazer hem flutter",
"face zoom in renewed focus",
"reflection in glass wall double image",
"low angle sun slice across floor"
]
}
]
},
{
"sceneId": 5,
"lyricAnchor": "bridge",
"time": "Early Afternoon",
"location": "City park bench under light trees",
"outfit": "blazer off, sleeves rolled, relaxed tie-less",
"lightFX": "dappled sun through leaves, pollen glitter bokeh, gentle breeze",
"cuts": [
{
"cutId": 1,
"pose": "sitting on bench",
"action": "unwraps simple sandwich",
"camera": [
"full body bench shot with leaf bokeh",
"side shot paper crinkle detail",
"face zoom in serene break",
"overhead lunch + watch on wrist",
"back shot path with cyclists light streaks"
]
},
{
"cutId": 2,
"pose": "takes first bite",
"action": "closes eyes a second",
"camera": [
"close bite with sun rim on cheek",
"profile crumbs, shallow DOF",
"face zoom in small smile",
"wide park pond sparkle",
"low angle grass blades glowing"
]
},
{
"cutId": 3,
"pose": "checks notebook",
"action": "jots quick line",
"camera": [
"macro ink glint",
"side shot wrist veins and light",
"overhead notebook grid clean",
"back shot kite in distant sky",
"face close pensive"
]
},
{
"cutId": 4,
"pose": "sips water",
"action": "exhales, shoulders drop",
"camera": [
"close water bottle condensation sparkles",
"profile sip with sun flare",
"wide bench with trees swaying",
"ground-level ants-eye leaves",
"face gentle relief"
]
},
{
"cutId": 5,
"pose": "stands to go",
"action": "tucks notebook into bag",
"camera": [
"back shot dust motes in sun beam",
"side shot bag flap catchlight",
"low angle steps from gravel",
"overhead bench now empty",
"wide exit through shimmering heat"
]
}
]
},
{
"sceneId": 6,
"lyricAnchor": "bridge → rap",
"time": "Afternoon",
"location": "Open office collaboration zone",
"outfit": "smart-casual with blazer on, sleeves half-rolled",
"lightFX": "big window shafts, whiteboard bounce, laptop screen glows",
"cuts": [
{
"cutId": 1,
"pose": "huddled with team",
"action": "points at tablet, laughs",
"camera": [
"three-quarter group with warm spill",
"side shot fingertip reflect on glass",
"face zoom in animated eyes",
"overhead table, sticky notes constellation",
"back shot sun flare between heads"
]
},
{
"cutId": 2,
"pose": "pair-programming vibe",
"action": "nods at colleagues suggestion",
"camera": [
"over-shoulder code lines glow",
"profile double-bounce light",
"face close appreciative smirk",
"wide office as soft bokeh",
"macro key tap with sparkle"
]
},
{
"cutId": 3,
"pose": "high-five moment",
"action": "quick celebratory clasp",
"camera": [
"hand slap in slow-mo micro flare",
"wide with cheering silhouettes",
"face zoom in spark of pride",
"back shot window halo",
"low angle chairs and shoe scuffs gleam"
]
},
{
"cutId": 4,
"pose": "whiteboard arrows",
"action": "draws bold box around goal",
"camera": [
"marker squeak macro with dust specks",
"side shot arm casting graphic shadow",
"face determined nod",
"overhead board geometry",
"wide team nodding"
]
},
{
"cutId": 5,
"pose": "break breath",
"action": "looks out window to skyline",
"camera": [
"back shot silhouette and city shimmer",
"profile rim light on jawline",
"face close hopeful",
"low angle blinds lines across suit",
"wide room with golden haze creep"
]
}
]
},
{
"sceneId": 7,
"LyricAnchor": "rap",
"time": "Late Afternoon",
"location": "Recording studio, vocal booth with pop filter and headphones",
"outfit": "black tee under blazer, dark jeans; studio sneakers",
"lightFX": "neon edge lights, VU meter glow, soft haze for beams",
"cuts": [
{
"cutId": 1,
"pose": "headphones on",
"action": "adjusts mic height behind pop filter",
"camera": [
"face close through pop filter mesh bokeh",
"side mic silhouette with LED rim",
"overhead booth foam pattern",
"back shot cable drape glint",
"macro hands rolling knob with light tick"
]
},
{
"cutId": 2,
"pose": "eyes closed",
"action": "raps first bar with steady breath",
"camera": [
"profile with breath not hitting mic (good distance)",
"face zoom in focused cadence",
"VU meter needles dancing",
"wide booth with purple-blue haze",
"low angle light bar lens flare"
]
},
{
"cutId": 3,
"pose": "punch-in moment",
"action": "signals engineer, resumes",
"camera": [
"over-shoulder engineer glass reflection",
"side shot finger count-in",
"face close confident nod",
"macro record light switching on",
"wide studio cables lines"
]
},
{
"cutId": 4,
"pose": "chorus take",
"action": "leans slightly, controlled power",
"camera": [
"three-quarter with mic halo",
"face zoom in grit + hope",
"low angle stand and shock mount glint",
"back shot waveforms on screen",
"overhead booth ceiling star LEDs"
]
},
{
"cutId": 5,
"pose": "final word",
"action": "exhales, half smile",
"camera": [
"face close de-compress",
"side shot headphone slide off one ear",
"macro stop button press",
"wide studio with color wash fade",
"back shot door open to warm hall"
]
}
]
},
{
"sceneId": 8,
"lyricAnchor": "verse B",
"time": "Evening",
"location": "City park path with family",
"outfit": "soft knit over shirt, dark chinos; family in cozy layers",
"lightFX": "golden hour glow, lens flare peeks, fairy-light bokeh",
"cuts": [
{
"cutId": 1,
"pose": "walking hand-in-hand",
"action": "gentle chat, smile",
"camera": [
"full body back shot into sun",
"side shot swinging hands in flare",
"face close warm laugh lines",
"overhead trees and light leaks",
"low angle shoes kicking leaves sparkle"
]
},
{
"cutId": 2,
"pose": "kid points at birds",
"action": "he kneels to show map",
"camera": [
"two-shot knee level with soft glow",
"face close parental pride",
"over-shoulder simple map sketch",
"wide path dotted with fairy lights",
"macro leaf veins lit"
]
},
{
"cutId": 3,
"pose": "group selfie",
"action": "click with laughter",
"camera": [
"phone POV smiling faces, sun starburst",
"side shot arm extended",
"back shot silhouettes + flare arc",
"overhead family circle",
"wide park twinkle lights on"
]
},
{
"cutId": 4,
"pose": "rest on bench",
"action": "water break, shared bottle",
"camera": [
"medium sharing gesture backlit",
"face close gentle gratitude",
"low angle bottle condensation spark",
"overhead bench wood grain glow",
"wide with city edge twinkling"
]
},
{
"cutId": 5,
"pose": "stand to leave",
"action": "quick group hug",
"camera": [
"tight hug with sun blooming",
"side shot hands around shoulders",
"back shot long shadows merge",
"overhead crown of light around heads",
"wide path leading toward skyline"
]
}
]
},
{
"sceneId": 9,
"lyricAnchor": "verse B",
"time": "Night",
"location": "Overlook with clean city nightscape",
"outfit": "blazer back on, scarf added",
"lightFX": "city bokeh, cool-blue sky, subtle haze for star glints",
"cuts": [
{
"cutId": 1,
"pose": "hands on railing",
"action": "breath visible, quiet awe",
"camera": [
"full body with skyline glitter",
"profile face with neon reflection in eyes",
"back shot shoulders squared",
"low angle railing shine + lens flare",
"overhead city grid lines"
]
},
{
"cutId": 2,
"pose": "checks notes on phone",
"action": "adds a single word: 'continue'",
"camera": [
"macro phone screen glow",
"side shot finger tap highlight",
"face close calm resolve",
"wide skyline as bokeh ocean",
"back shot scarf ripple"
]
},
{
"cutId": 3,
"pose": "exhale slowly",
"action": "pockets hands",
"camera": [
"profile fogged breath in neon halo",
"low angle shoe on concrete edge",
"face zoom in quiet smile",
"overhead subtle city ray streak",
"wide with passing light trail"
]
},
{
"cutId": 4,
"pose": "turns to camera",
"action": "small nod forward",
"camera": [
"head-on medium with flare arch",
"close eyes determined brightness",
"side jawline rim light",
"back shot then whip-pan reveal city",
"wide crane up, city expands"
]
},
{
"cutId": 5,
"pose": "steps away",
"action": "walks along overlook path",
"camera": [
"tracking back shot bokeh river",
"side shot rail lights passing",
"face three-quarter confident",
"low angle puddle reflecting skyline",
"overhead path curve luminous"
]
}
]
},
{
"sceneId": 10,
"lyricAnchor": "rap → end",
"time": "Night",
"location": "Office district after hours",
"outfit": "blazer + scarf, gloves optional",
"lightFX": "glass facade reflections, wet pavement neon, gentle drizzle mist",
"cuts": [
{
"cutId": 1,
"pose": "walking alone between towers",
"action": "steady stride, chin up",
"camera": [
"full body on wet pavement mirror",
"side shot droplets catching light",
"face close focused calm",
"back shot towers converging lines",
"overhead umbrella silhouettes distant"
]
},
{
"cutId": 2,
"pose": "pauses at intersection",
"action": "inhales cool air",
"camera": [
"low angle traffic light glow on mist",
"profile breath plume",
"wide glass reflections kaleidoscope",
"macro droplet slide on sleeve",
"face close half-smile"
]
},
{
"cutId": 3,
"pose": "checks tram schedule afar",
"action": "decides to walk instead",
"camera": [
"long lens heat-haze shimmer line",
"back shot step away from stop",
"side shot rhythmic footfalls",
"wide lone figure + city hum",
"overhead rain dot glitter"
]
},
{
"cutId": 4,
"pose": "crosswalk stride",
"action": "bag strap adjustment",
"camera": [
"ground reflection of zebra stripes",
"three-quarter with neon banding",
"face close rain specks on cheek",
"low angle heel-toe cadence",
"wide car lights streak behind"
]
},
{
"cutId": 5,
"pose": "turns corner",
"action": "street opens to quieter block",
"camera": [
"back shot disappearing into warm alley glow",
"side shot brick wall glisten",
"overhead pocket of golden steam vent",
"face three-quarter softened by warmth",
"wide fade to gentle drizzle veil"
]
}
]
},
{
"sceneId": 11,
"lyricAnchor": "end",
"time": "Late Night",
"location": "Home living room, cozy lamp pools",
"outfit": "cardigan over tee, soft pants, socks; glasses on table",
"lightFX": "warm lamps, TV ambient glow, window rain reflections",
"cuts": [
{
"cutId": 1,
"pose": "sinks into sofa",
"action": "removes watch, exhales",
"camera": [
"full body cozy frame with lamp bloom",
"macro watch buckle catchlight",
"face close relief",
"overhead coffee table still life",
"back shot curtain rain trails"
]
},
{
"cutId": 2,
"pose": "scrolls photos",
"action": "smiles at family selfie",
"camera": [
"close phone screen glow on eyes",
"profile soft grin",
"wide living room warm pools",
"low angle socks on rug texture",
"back shot window city twinkle"
]
},
{
"cutId": 3,
"pose": "leans back",
"action": "sips late tea",
"camera": [
"macro steam ribbon in lamp light",
"side shot cup handle highlight",
"face close calm closure",
"overhead tray symmetry",
"wide gentle rack focus to rain"
]
},
{
"cutId": 4,
"pose": "turns off lamp",
"action": "room dims to TV glow",
"camera": [
"finger on switch micro flare",
"silhouette against window bokeh",
"face close eyes soft",
"low angle shadows lengthen",
"wide night hush settles"
]
},
{
"cutId": 5,
"pose": "final glance to window",
"action": "small nod—tomorrow again",
"camera": [
"face three-quarter with faint smile",
"back shot rain tracing lines of light",
"macro drop racing down glass",
"wide living room serene",
"fade out on city reflections"
]
}
]
}
]
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,272 @@
{
"character": {
"bodyType": "slim and youthful",
"hairStyle": "long wavy hair with pastel blue and purple colors"
},
"scenes": [
{
"sceneId": 1,
"time": "Morning",
"location": "Cozy bedroom with pastel bedding and sunlight through curtains",
"outfit": "white oversized pajama shirt with thigh-high stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying on bed with head resting on hands",
"action": "smiling softly at the camera",
"camera": [
"overhead shot from above capturing sunlight on her hair",
"close-up of her face with shallow depth of field",
"side angle showing body stretched on bed",
"slow zoom-in from doorway",
"handheld camera wobble for intimate feeling"
]
},
{
"cutId": 2,
"pose": "stretching arms above head while sitting on bed",
"action": "yawning cutely with eyes half closed",
"camera": [
"medium shot from foot of bed",
"low angle from floor emphasizing legs",
"wide shot with window light flaring",
"tracking shot circling around her stretch",
"soft focus tilt-shift framing"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with playful look",
"action": "blowing a kiss to the camera",
"camera": [
"front close-up catching kiss in slow motion",
"wide shot with pastel background",
"camera tilt from below lips to eyes",
"360° pan around her kneeling pose",
"handheld push-in as she blows kiss"
]
}
]
},
{
"sceneId": 2,
"time": "Morning",
"location": "Bedroom near window with plants and sunlight",
"outfit": "light pastel camisole with short pleated skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting on window sill, legs slightly crossed",
"action": "looking outside then turning to smile",
"camera": [
"silhouette against sunlight",
"side profile with plants blurred in foreground",
"close-up of smile turning to camera",
"dolly-in from outside window glass",
"over-shoulder shot capturing view outside"
]
},
{
"cutId": 2,
"pose": "leaning against the wall, hands behind back",
"action": "giggling with shy expression",
"camera": [
"eye-level shot with wall texture visible",
"slight high angle to emphasize cuteness",
"medium close-up on giggle with tilt",
"rack focus between wall décor and her face",
"soft handheld sway left to right"
]
},
{
"cutId": 3,
"pose": "lying on floor with legs up against the wall",
"action": "kicking feet playfully while laughing",
"camera": [
"top-down view from ceiling",
"low angle from foot level",
"side shot capturing playful kicks",
"slow pan across her body",
"handheld camera zoom-in to laughter"
]
}
]
},
{
"sceneId": 3,
"time": "Afternoon",
"location": "Sunny street with pastel shops and flowers",
"outfit": "short pastel pink dress with white stockings and sneakers",
"cuts": [
{
"cutId": 1,
"pose": "standing with one hand on hip",
"action": "spinning gently in place",
"camera": [
"full body wide shot with pastel shops",
"low angle capturing dress swirl",
"tracking shot circling spin",
"handheld slow zoom-in to face",
"rear shot revealing spin from behind"
]
},
{
"cutId": 2,
"pose": "walking with small steps",
"action": "waving happily at the camera",
"camera": [
"tracking dolly shot in front",
"overhead drone shot of street",
"side follow shot at hip level",
"handheld jitter to mimic vlog",
"close-up on waving hand with face blurred"
]
},
{
"cutId": 3,
"pose": "leaning forward playfully",
"action": "making a heart shape with hands",
"camera": [
"tight close-up on hands forming heart",
"fish-eye wide close-up",
"side shot at 45°",
"POV shot as if receiving heart",
"zoom burst effect from wide to close"
]
}
]
},
{
"sceneId": 4,
"time": "Afternoon",
"location": "Trendy café with bright modern interior",
"outfit": "white blouse tucked into pleated skirt with stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting at a table with chin on hands",
"action": "smiling softly while tilting head",
"camera": [
"front eye-level close-up",
"soft focus on eyes, blurred background",
"over-shoulder shot of coffee cup",
"panning shot across table to her",
"low angle from table surface"
]
},
{
"cutId": 2,
"pose": "standing by window with crossed legs",
"action": "writing playfully in a small notebook",
"camera": [
"profile with sunlight flare",
"close-up of pen on paper then tilt to face",
"tracking shot from feet to head",
"reflection in window glass",
"medium shot with bokeh lights behind"
]
},
{
"cutId": 3,
"pose": "sitting sideways on chair with one leg up",
"action": "taking a sip from a cup with cute expression",
"camera": [
"tight focus on lips touching cup",
"medium shot framed by chair back",
"slight dutch angle for energy",
"slow dolly-in on playful sip",
"wide establishing shot of café"
]
}
]
},
{
"sceneId": 5,
"time": "Night",
"location": "Elegant ballroom with chandeliers",
"outfit": "sparkly silver mini dress with black stockings and heels",
"cuts": [
{
"cutId": 1,
"pose": "standing tall with one hand on waist",
"action": "turning slowly while smiling confidently",
"camera": [
"wide shot capturing chandelier",
"low angle emphasizing elegance",
"tracking dolly rotation",
"close-up of smile during turn",
"rear tracking shot revealing gown shimmer"
]
},
{
"cutId": 2,
"pose": "sitting gracefully on a velvet chair",
"action": "crossing legs elegantly",
"camera": [
"medium close-up from side",
"top-down angle showing chair texture",
"front focus on crossed legs",
"soft rack focus from chair to her face",
"panning shot circling chair"
]
},
{
"cutId": 3,
"pose": "leaning on railing with dreamy look",
"action": "gazing at the chandelier lights",
"camera": [
"over-shoulder shot of chandelier view",
"profile silhouette with golden backlight",
"wide shot capturing ballroom depth",
"handheld tilt up from railing to face",
"slow dolly-out revealing emptiness of hall"
]
}
]
},
{
"sceneId": 6,
"time": "Night",
"location": "Luxurious bedroom with soft golden lighting",
"outfit": "black lace camisole with mini skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying sideways on bed with legs slightly bent",
"action": "looking at the camera with sultry eyes",
"camera": [
"close-up on eyes with blurred background",
"tracking shot along legs up to face",
"overhead soft focus",
"low angle from bed surface",
"handheld intimate pan across body"
]
},
{
"cutId": 2,
"pose": "sitting at vanity table",
"action": "putting on earrings slowly",
"camera": [
"mirror reflection focus",
"close-up on hands adjusting earrings",
"profile side shot with warm glow",
"over-shoulder shot including vanity lights",
"slow push-in from doorway"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with arched back",
"action": "running hand through hair with sensual expression",
"camera": [
"rear shot with back arch emphasized",
"medium close-up focusing on hair movement",
"low angle capturing curves",
"soft focus candlelight bokeh",
"circling dolly shot for dramatic effect"
]
}
]
}
]
}

Binary file not shown.

View File

@ -0,0 +1,296 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { logger } from '../../lib/logger';
import { callOpenAI } from '../../lib/openai';
import { callLMStudio } from '../../lib/lmstudio';
// import { generateImage as generateFaceImage } from '../lib/image-generator-face'; // Removed
import { generateImage } from '../../lib/image-generator'; // Added
dotenv.config();
type Size = { width: number; height: number };
interface MusicSpotCharacter {
bodyType: string;
hairStyle: string;
}
interface MusicSpotCut {
cutId: number;
pose: string;
action: string;
camera?: string[]; // list of camera variants per cut
}
interface MusicSpotScene {
sceneId: number;
time: string;
location: string;
outfit: string;
cuts: MusicSpotCut[];
}
interface MusicSpotConfig {
character: MusicSpotCharacter;
scenes: MusicSpotScene[];
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 1280, height: 720 };
const FOLDER = process.argv[2] || process.env.MUSICSPOT_FOLDER || 'infinitydance';
const FOLDER_SAFE = FOLDER.replace(/[/\\?%*:|"<>]/g, '_');
const FACE_SRC = path.resolve(`src/musicspot_generator/${FOLDER}/face.png`);
const GENERATED_DIR = path.resolve('generated');
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},
/*
{
name: 'SERVER2',
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
}, */
]
.filter((s) => !!s.baseUrl && !!s.outputDir)
.map((s) => ({
...s,
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
async function copyFaceToServers(servers: Server[]): Promise<string | undefined> {
const faceFileName = 'face.png';
// Validate face source
try {
await fs.access(FACE_SRC);
} catch {
// If face.png doesn't exist, we don't need to copy anything.
// The caller should handle this case.
return undefined;
}
for (const srv of servers) {
if (!srv.inputDir) continue;
const dest = path.join(srv.inputDir, faceFileName);
try {
await fs.mkdir(srv.inputDir, { recursive: true });
await fs.copyFile(FACE_SRC, dest);
logger.info(`Copied face image to ${srv.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy face image to ${srv.name}: ${err}`);
}
}
return faceFileName; // Comfy expects file name present in input dir
}
function buildImagePromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string,
hasFaceImage: boolean // Added parameter
): string {
let promptInstructions = `
Write "imagePrompt" in around 110140 words to generate a still portrait image (720x1280 vertical).
`;
if (hasFaceImage) {
promptInstructions += `Keep a consistent character identity using the provided face image (identity preservation), but do not mention any camera brand/model.
`;
} else {
promptInstructions += `Do not use any face image for identity preservation.
`;
}
return `
Return exactly one JSON object, nothing else: { "imagePrompt": "Cinematic realistic photo, (camera framing),(character),(pose),(time),(location),(outfit),(action),(lighting)" }.
${promptInstructions}
Describe clearly and concretely:
- Character: ${character.bodyType}; hair: ${character.hairStyle}
- Camera framing/composition intention: ${cameraIntent}
- Time: ${scene.time}
- Location: ${scene.location}
- Outfit: ${scene.outfit}
- Pose: ${cut.pose}
- Action/Expression: ${cut.action}
- Lighting: please be creative and make beautiful lighting, I like something like luminous, colorful
Important:
- adjust the outfit based on the camera framing, describe only what is visible.
For example, if the framing is a close-up of the face, do not mention the outfit at all.
Only respond with JSON.
`.trim();
}
async function getImagePromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.imagePrompt || res?.image_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return imagePrompt JSON.');
}
return prompt.trim();
}
const IMAGE_PROMPT_PROVIDER = (process.env.IMAGE_PROMPT_PROVIDER || 'lmstudio').toLowerCase();
async function getImagePromptFromLMStudio(req: string): Promise<string> {
const res = await callLMStudio(req);
const prompt = res?.imagePrompt || res?.image_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('LM Studio failed to return imagePrompt JSON.');
}
return prompt.trim();
}
async function getImagePrompt(req: string): Promise<string> {
if (IMAGE_PROMPT_PROVIDER === 'openai') {
return getImagePromptFromOpenAI(req);
}
// default to LM Studio
return getImagePromptFromLMStudio(req);
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function main() {
try {
await ensureDirs();
// Check if face.png exists
let faceImageExists = false;
try {
await fs.access(FACE_SRC);
faceImageExists = true;
logger.info(`Face image found at ${FACE_SRC}`);
} catch {
logger.warn(`Face image not found at ${FACE_SRC}. Images will be generated without face conditioning.`);
}
// Load scenes.json
const configRaw = await fs.readFile(path.resolve(`src/musicspot_generator/${FOLDER}/scenes.json`), 'utf-8');
const cfg: MusicSpotConfig = JSON.parse(configRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Ensure face.png in each server's input, only if it exists
let faceFileName: string | undefined = undefined;
if (faceImageExists) {
faceFileName = await copyFaceToServers(servers);
} else {
logger.info('Skipping copyFaceToServers as face image does not exist.');
}
// Generate images only (no video here). Intended to be run first.
let imageTaskIndex = 0;
for (const scene of cfg.scenes) {
logger.info(`=== Scene ${scene.sceneId}: Image generation start ===`);
for (const cut of scene.cuts) {
const cameraVariants =
Array.isArray(cut.camera) && cut.camera.length > 0
? cut.camera
: ['eye-level medium shot', 'slight left 30°', 'slight right 30°', 'slight high angle', 'slight low angle'];
for (let camIdx = 0; camIdx < cameraVariants.length; camIdx++) {
const cameraIntent = cameraVariants[camIdx];
const variantIndex = camIdx + 1;
const imgFileName = `${FOLDER_SAFE}_musicspot_s${scene.sceneId}_c${cut.cutId}_v${variantIndex}.png`;
const outputPath = path.join(GENERATED_DIR, imgFileName);
// Skip generation if target file already exists
try {
await fs.access(outputPath);
logger.info(`Skipping generation, file already exists: ${outputPath}`);
continue;
} catch {
// File does not exist; proceed with generation
}
// 1) Generate image prompt for this camera
logger.info(`Scene ${scene.sceneId} - Cut ${cut.cutId} - Cam${variantIndex}: generating image prompt...`);
// Pass faceImageExists to buildImagePromptRequest
const imgPromptReq = buildImagePromptRequest(cfg.character, scene, cut, cameraIntent, faceImageExists);
let imagePrompt: string;
try {
imagePrompt = await getImagePrompt(imgPromptReq);
} catch (err) {
logger.error(
`${IMAGE_PROMPT_PROVIDER.toUpperCase()} image prompt failed for scene ${scene.sceneId} cut ${cut.cutId} cam ${variantIndex}: ${err}`
);
continue;
}
// 2) Generate one image using the new generateImage function
const serverForImage = pickServer(servers, imageTaskIndex++);
logger.info(`Generating image (${imgFileName}) on ${serverForImage.name}...`);
try {
// Use the generic generateImage function from image-generator.ts
// The faceFileName is no longer passed as an argument, but its existence
// influenced the imagePrompt.
const finalImagePath = await generateImage(
imagePrompt, // The prompt now contains face instructions if faceImageExists is true
imgFileName,
serverForImage.baseUrl!,
serverForImage.outputDir!,
'flux', // Use default imageModel ('qwen')
DEFAULT_SIZE
);
logger.info(`Image generated: ${finalImagePath}`);
} catch (err) {
logger.error(`Image generation failed (${imgFileName}) on ${serverForImage.name}: ${err}`);
continue;
}
}
}
logger.info(`=== Scene ${scene.sceneId}: Image generation complete ===`);
}
logger.info('Image generation for all scenes completed.');
} catch (err) {
logger.error('Fatal error in music spot image generator:', err);
}
}
main().catch((err) => {
logger.error('Unhandled error:', err);
});

View File

@ -0,0 +1,366 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { logger } from '../../lib/logger';
import { callOpenAI } from '../../lib/openai';
import { generateImage as generateFaceImage } from '../../lib/image-generator-face';
import { generateVideo } from '../../lib/video-generator';
dotenv.config();
type Size = { width: number; height: number };
interface MusicSpotCharacter {
bodyType: string;
hairStyle: string;
}
interface MusicSpotCut {
cutId: number;
pose: string;
action: string;
camera?: string[]; // list of 5 camera variants per cut
}
interface MusicSpotScene {
sceneId: number;
time: string;
location: string;
outfit: string;
cuts: MusicSpotCut[];
}
interface MusicSpotConfig {
character: MusicSpotCharacter;
scenes: MusicSpotScene[];
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 720, height: 1280 };
const FACE_SRC = path.resolve('src/musicspot_generator/face.png');
const GENERATED_DIR = path.resolve('generated');
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
}
/*{
name: 'SERVER2',
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
}*/,
]
.filter(s => !!s.baseUrl && !!s.outputDir)
.map(s => ({
...s,
// Convert output dir to input dir by convention
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
async function copyFaceToServers(servers: Server[]): Promise<{ fileName: string; absPerServer: Record<string, string> }> {
const faceFileName = 'face.png';
const absPerServer: Record<string, string> = {};
// Validate face source
try {
await fs.access(FACE_SRC);
} catch {
throw new Error(`Face image not found at ${FACE_SRC}`);
}
for (const srv of servers) {
if (!srv.inputDir) continue;
const dest = path.join(srv.inputDir, faceFileName);
try {
await fs.mkdir(srv.inputDir, { recursive: true });
await fs.copyFile(FACE_SRC, dest);
absPerServer[srv.name] = path.resolve(dest);
logger.info(`Copied face image to ${srv.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy face image to ${srv.name}: ${err}`);
}
}
return { fileName: faceFileName, absPerServer };
}
function buildImagePromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string
): string {
// Ask OpenAI to return JSON: { "imagePrompt": "..." }
return `
Return exactly one JSON object, nothing else: { "imagePrompt": "..." }.
Write "imagePrompt" in around 110140 words to generate a still portrait image (720x1280 vertical).
Keep a consistent character identity using the provided face image (identity preservation), but do not mention any camera brand/model.
Describe clearly and concretely:
- Character: ${character.bodyType}; hair: ${character.hairStyle}
- Time: ${scene.time}
- Location: ${scene.location}
- Outfit: ${scene.outfit}
- Pose: ${cut.pose}
- Action/Expression: ${cut.action}
- Camera framing/composition intention: ${cameraIntent}
- Lighting/mood/style: cohesive and realistic, natural skin tones, soft depth of field.
Avoid: brand names, copyrighted characters, extreme or explicit content, text overlays, watermarks, multiple people. Focus on a single subject medium-full portrait, tasteful and aesthetic. Use simple sentences.
Only respond with JSON.
`.trim();
}
function buildVideoPromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string
): string {
// Ask OpenAI to return JSON: { "videoPrompt": "..." }
// Strong constraints to avoid "cut/zoom" etc. Keep a single continuous 8s shot.
return `
Return exactly one JSON object and nothing else: { "videoPrompt": "..." }.
Write "videoPrompt" in 100140 words. Present tense. Concrete, simple sentences.
HARD RULES:
- One continuous 8-second shot (oner). No edits.
- Fixed location and general vantage; maintain spatial continuity.
- No zooms, no rack zoom, no smash/push-in, no cuts, no transitions, no "meanwhile".
- Camera motion: at most a slight pan/tilt or subtle dolly within 1 meter.
- Keep framing consistent (vertical 720x1280). Avoid technical brand names or lens jargon.
Incorporate the following camera intention: "${cameraIntent}".
If it conflicts with HARD RULES (e.g., zoom, push-in, extreme moves), reinterpret it into a subtle, compliant motion (e.g., gentle glide, slight pan/tilt) while preserving the creative intent.
Describe:
1) Main action: ${cut.action}
2) Pose/composition: ${cut.pose}
3) Scene/time/location/outfit: ${scene.time}; ${scene.location}; outfit: ${scene.outfit}
4) Lighting/mood/style coherent with the character: ${character.bodyType}; hair: ${character.hairStyle}
Prohibited (case-insensitive): cut, cuts, cutting, quick cut, insert, close-up, extreme close-up, zoom, zooming, push-in, pull-out, whip, switch angle, change angle, montage, cross-cut, smash cut, transition, meanwhile, later.
Only respond with JSON.
`.trim();
}
async function getImagePromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.imagePrompt || res?.image_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return imagePrompt JSON.');
}
return prompt.trim();
}
async function getVideoPromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.videoPrompt || res?.video_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return videoPrompt JSON.');
}
return prompt.trim();
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function copyImageToAllServerInputs(servers: Server[], localGeneratedImagePath: string): Promise<string> {
const fileName = path.basename(localGeneratedImagePath);
const copies: string[] = [];
for (const s of servers) {
if (!s.inputDir) continue;
const dest = path.join(s.inputDir, fileName);
try {
await fs.copyFile(localGeneratedImagePath, dest);
copies.push(dest);
logger.debug(`Copied ${fileName} to ${s.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy ${fileName} to ${s.name} input: ${err}`);
}
}
return fileName; // return the name used for Comfy workflows
}
async function main() {
try {
await ensureDirs();
// Load scenes.json
const configRaw = await fs.readFile(path.resolve('src/musicspot_generator/scenes.json'), 'utf-8');
const cfg: MusicSpotConfig = JSON.parse(configRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Ensure face.png in each server's input
const { fileName: faceFileName } = await copyFaceToServers(servers);
// Two-phase per scene:
// Phase 1: Generate ALL images for the scene (to keep image model warm)
// Phase 2: Generate ALL videos for the images of that scene (to keep video model warm)
let imageTaskIndex = 0;
let videoTaskIndex = 0;
for (const scene of cfg.scenes) {
logger.info(`=== Scene ${scene.sceneId}: Phase 1 - Image generation start ===`);
type SceneImageItem = {
sceneId: number;
cutId: number;
cameraIntent: string;
imgFileName: string;
imgPath: string;
};
const sceneImageItems: SceneImageItem[] = [];
// Phase 1: images
for (const cut of scene.cuts) {
const cameraVariants = Array.isArray(cut.camera) && cut.camera.length > 0
? cut.camera
: ['eye-level medium shot', 'slight left 30°', 'slight right 30°', 'slight high angle', 'slight low angle'];
for (let camIdx = 0; camIdx < cameraVariants.length; camIdx++) {
const cameraIntent = cameraVariants[camIdx];
const variantIndex = camIdx + 1;
// 1) Generate image prompt for this camera
logger.info(`Scene ${scene.sceneId} - Cut ${cut.cutId} - Cam${variantIndex}: generating image prompt...`);
const imgPromptReq = buildImagePromptRequest(cfg.character, scene, cut, cameraIntent);
let imagePrompt: string;
try {
imagePrompt = await getImagePromptFromOpenAI(imgPromptReq);
} catch (err) {
logger.error(`OpenAI image prompt failed for scene ${scene.sceneId} cut ${cut.cutId} cam ${variantIndex}: ${err}`);
continue;
}
// 2) Generate one image using face conditioning for this specific camera
const serverForImage = pickServer(servers, imageTaskIndex++);
const imgFileName = `musicspot_s${scene.sceneId}_c${cut.cutId}_v${variantIndex}.png`;
logger.info(`Generating image (${imgFileName}) on ${serverForImage.name}...`);
try {
// Use only the face file name for the workflow image input (Comfy expects it in its input dir)
const finalImagePath = await generateFaceImage(
imagePrompt,
faceFileName,
imgFileName,
serverForImage.baseUrl!,
serverForImage.outputDir!,
DEFAULT_SIZE
);
logger.info(`Image generated: ${finalImagePath}`);
sceneImageItems.push({
sceneId: scene.sceneId,
cutId: cut.cutId,
cameraIntent,
imgFileName,
imgPath: finalImagePath,
});
} catch (err) {
logger.error(`Image generation failed (${imgFileName}) on ${serverForImage.name}: ${err}`);
continue;
}
}
}
logger.info(`=== Scene ${scene.sceneId}: Phase 1 complete. Generated ${sceneImageItems.length} image(s). ===`);
logger.info(`=== Scene ${scene.sceneId}: Phase 2 - Video generation start ===`);
// Phase 2: videos for all images generated in this scene
for (const item of sceneImageItems) {
// Find original cut info for prompts
const cut = scene.cuts.find(c => c.cutId === item.cutId);
if (!cut) {
logger.warn(`Cut ${item.cutId} not found for scene ${scene.sceneId}, skipping video.`);
continue;
}
// 3) Generate video prompt for this camera
logger.info(`Generating video prompt for ${item.imgFileName} (scene ${scene.sceneId}, cut ${item.cutId})...`);
const vidPromptReq = buildVideoPromptRequest(cfg.character, scene, cut, item.cameraIntent);
let videoPrompt: string;
try {
videoPrompt = await getVideoPromptFromOpenAI(vidPromptReq);
} catch (err) {
logger.error(`OpenAI video prompt failed for ${item.imgFileName}: ${err}`);
continue;
}
// 4) Copy the base image to every server's input folder
const imageFileNameForComfy = await copyImageToAllServerInputs(servers, item.imgPath);
// 5) Generate video on a chosen server (round-robin)
const serverForVideo = pickServer(servers, videoTaskIndex++);
const videoFileName = item.imgFileName.replace(/\.png$/i, '.mp4');
logger.info(`Generating video (${videoFileName}) on ${serverForVideo.name} using ${imageFileNameForComfy}...`);
try {
const videoPath = await generateVideo(
videoPrompt,
imageFileNameForComfy,
videoFileName,
serverForVideo.baseUrl!,
serverForVideo.outputDir!,
DEFAULT_SIZE
);
logger.info(`Video generated: ${videoPath}`);
} catch (err) {
logger.error(`Video generation failed (${videoFileName}) on ${serverForVideo.name}: ${err}`);
}
}
logger.info(`=== Scene ${scene.sceneId}: Phase 2 complete. ===`);
}
logger.info('Music spot generation completed.');
} catch (err) {
logger.error('Fatal error in music spot generator:', err);
} finally {
// Optional: do not exit to allow long-running processes; but align with other scripts:
// process.exit();
}
}
main().catch(err => {
logger.error('Unhandled error:', err);
// process.exit(1);
});

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,827 @@
{
"character": {
"bodyType": "slim and youthful",
"hairStyle": "long straight blonde hair, white skin, blue eyes, light makeup"
},
"scenes": [
{
"sceneId": 1,
"time": "Evening",
"location": "Recording studio with warm lighting, microphone, pop filter, and soundproof walls",
"outfit": "cute pastel dress with stockings and studio-style headphones",
"cuts": [
{
"cutId": 1,
"pose": "standing close to microphone with both hands on headphones",
"action": "singing passionately with eyes closed",
"camera": [
"front medium shot centered on microphone and pop filter",
"close-up on her lips behind pop filter",
"side profile with blurred studio lights",
"low angle capturing hands holding headphones",
"over-shoulder shot showing recording booth perspective"
]
},
{
"cutId": 2,
"pose": "leaning slightly forward into microphone",
"action": "singing softly with focused expression",
"camera": [
"tight close-up through pop filter mesh",
"side close-up emphasizing focus in her eyes",
"rear shot with blurred soundboard in background",
"medium shot with studio monitors visible",
"handheld dolly-in for intimate effect"
]
},
{
"cutId": 3,
"pose": "one hand holding microphone stand, other hand raised slightly",
"action": "singing energetically with emotion",
"camera": [
"front low angle emphasizing raised hand",
"wide shot showing booth environment",
"profile shot with dramatic lighting",
"close-up on hand gripping mic stand",
"rear shot with blurred pop filter frame"
]
},
{
"cutId": 4,
"pose": "adjusting headphones with one hand while singing",
"action": "smiling softly between lyrics",
"camera": [
"front close-up on headphones adjustment",
"side profile smile with blurred mic",
"tight close-up on eyes sparkling",
"rear dolly shot focusing on headphones",
"medium shot with warm studio lights behind"
]
},
{
"cutId": 5,
"pose": "sitting on studio stool in front of mic",
"action": "closing eyes and holding lyrics sheet while singing",
"camera": [
"front shot framing mic and lyric sheet",
"close-up on lyric sheet in hand",
"side shot capturing relaxed pose",
"rear shot with blurred mixing desk",
"wide establishing shot of whole studio booth"
]
}
]
},
{
"sceneId": 2,
"time": "Day",
"location": "Minimalistic modern recording studio, all white walls and furniture, microphone with pop filter and sound panels",
"outfit": "elegant pure white dress with sheer stockings and studio headphones",
"cuts": [
{
"cutId": 1,
"pose": "standing gracefully in front of white microphone setup",
"action": "singing with serene expression, both hands gently on headphones",
"camera": [
"front medium shot with pure white background",
"close-up on serene face framed by white headphones",
"side profile with white mic and pop filter",
"low angle emphasizing elegance of white dress",
"rear over-shoulder shot into white booth"
]
},
{
"cutId": 2,
"pose": "leaning slightly toward microphone stand",
"action": "singing softly with eyes closed",
"camera": [
"tight close-up through white pop filter mesh",
"profile capturing closed eyes and focus",
"rear shot showing clean white acoustic panels",
"medium shot with glowing white lighting",
"slow dolly-in on her face from side"
]
},
{
"cutId": 3,
"pose": "raising one hand gracefully in the air while singing",
"action": "expressing emotion with body movement",
"camera": [
"front low angle emphasizing raised hand",
"wide shot of white studio environment",
"side profile with flowing dress detail",
"close-up on extended hand in soft light",
"rear shot capturing silhouette in white glow"
]
},
{
"cutId": 4,
"pose": "adjusting headphones gently with one hand",
"action": "smiling subtly between lyrics",
"camera": [
"front close-up with headphones in frame",
"side profile smile against white wall",
"tight close-up on sparkling eyes",
"medium shot with microphone blurred in foreground",
"over-shoulder capturing white minimalist interior"
]
},
{
"cutId": 5,
"pose": "sitting elegantly on modern white stool",
"action": "holding lyric sheet in lap while singing softly",
"camera": [
"front shot framing mic and lyric sheet",
"close-up on hands holding paper",
"side shot with white studio lights glowing",
"rear shot showing full white booth setup",
"wide establishing shot of minimal white studio"
]
}
]
},
{
"sceneId": 3,
"time": "Afternoon",
"location": "Spacious dance studio with mirrored walls, wooden floor, and bright ceiling lights",
"outfit": "sporty outfit: cropped pastel hoodie with black track pants and white sneakers",
"appearance": "hair wet and clinging to face from sweat, skin glowing with perspiration",
"cuts": [
{
"cutId": 1,
"pose": "standing in front of mirror with hands on knees",
"action": "breathing heavily, sweat face, wet hair",
"camera": [
"front medium shot showing sweat on face",
"rear shot capturing reflection in mirror",
"low angle emphasizing strong posture",
"side close-up on sweat dripping",
"handheld shaky cam for intensity"
]
},
{
"cutId": 2,
"pose": "leaning back slightly with arms raised",
"action": "stretching arms after intense dancing",
"camera": [
"front wide shot with arms extended",
"side profile with sweat-soaked hair",
"low angle highlighting arms and torso",
"rear shot with mirrored reflection",
"close-up on exhausted but determined face"
]
},
{
"cutId": 3,
"pose": "mid-dance move, sliding one foot forward",
"action": "serious expression, body low to ground",
"camera": [
"front wide shot capturing dynamic motion",
"low angle emphasizing movement power",
"side tracking following slide motion",
"rear shot reflecting in mirrors",
"close-up on concentrated eyes"
]
},
{
"cutId": 4,
"pose": "sitting on floor with legs stretched out",
"action": "wiping sweat from forehead with towel",
"camera": [
"close-up on towel wiping sweat",
"side profile with hair clinging to cheek",
"rear shot showing reflection in mirror",
"wide establishing studio shot",
"handheld zoom-in on tired smile"
]
},
{
"cutId": 5,
"pose": "crouching low with one hand on floor",
"action": "finishing dance move with powerful stance",
"camera": [
"front low angle emphasizing strength",
"side profile showing sweat flying off",
"rear shot with doubled mirror reflection",
"medium shot focusing on determined eyes",
"handheld circular shot for energy"
]
}
]
},
{
"sceneId": 4,
"time": "Morning",
"location": "Park with trees and soft mist",
"outfit": "pastel knit sweater with checkered mini skirt and black stockings",
"cuts": [
{
"cutId": 1,
"pose": "walking slowly on misty path",
"action": "touching leaves as she passes",
"camera": [
"wide shot with mist and sunrays",
"rear tracking shot through trees",
"close-up of hand brushing leaves",
"side profile with foggy background",
"drone rising above misty path"
]
},
{
"cutId": 2,
"pose": "standing still among trees",
"action": "stretching arms out to feel the morning air",
"camera": [
"front wide shot with rays filtering",
"over-shoulder capturing mist depth",
"low angle emphasizing trees and sky",
"medium shot with fog swirling around",
"handheld pan upward following arms"
]
},
{
"cutId": 3,
"pose": "sitting on wooden bench",
"action": "exhaling softly and smiling peacefully",
"camera": [
"side profile with mist behind",
"close-up on serene smile",
"rear shot framing bench and fog",
"slow dolly-in through mist",
"wide landscape shot with her small figure"
]
},
{
"cutId": 4,
"pose": "kneeling on grass with hands in dew",
"action": "looking at water drops sparkling",
"camera": [
"macro close-up of dew on grass",
"low angle framing her hands",
"profile shot with blurred trees",
"overhead soft focus on her and ground",
"slow tilt from grass to her eyes"
]
},
{
"cutId": 5,
"pose": "walking across a small wooden bridge",
"action": "pausing to lean on railing and smile",
"camera": [
"wide shot of bridge in mist",
"rear tracking her walk",
"medium shot of leaning pose",
"close-up on gentle smile",
"drone reveal showing forest below"
]
}
]
},
{
"sceneId": 5,
"time": "Afternoon",
"location": "Flower field under bright blue sky",
"outfit": "pastel pink one-piece dress with white lace stockings",
"cuts": [
{
"cutId": 1,
"pose": "standing with arms open wide",
"action": "spinning slowly among flowers",
"camera": [
"wide drone shot circling her spin",
"low angle capturing flowers and sky",
"tracking shot through flowers toward her",
"rear silhouette against sunlit sky",
"close-up on smiling face with flowers blurred"
]
},
{
"cutId": 2,
"pose": "kneeling among flowers",
"action": "gently touching petals with fingers",
"camera": [
"macro close-up of fingers on petals",
"profile shot with blurred blooms",
"overhead capturing flower pattern",
"low angle with flowers framing face",
"slow dolly-in through blossoms"
]
},
{
"cutId": 3,
"pose": "lying on back in field",
"action": "looking up at clouds and laughing",
"camera": [
"overhead birds-eye view",
"close-up on laughing face with flowers",
"side shot across grass level",
"rear shot with blue sky backdrop",
"handheld zoom-in with shaky intimacy"
]
},
{
"cutId": 4,
"pose": "walking barefoot through flowers",
"action": "lifting skirt slightly while smiling",
"camera": [
"low angle on feet in grass",
"wide tracking shot from side",
"rear dolly-in following footsteps",
"medium shot with skirt lift motion",
"close-up on glowing smile"
]
},
{
"cutId": 5,
"pose": "sitting cross-legged in flowers",
"action": "making a small flower crown",
"camera": [
"front close-up on crown weaving",
"side profile with blurred blooms",
"top-down focusing on hands",
"rack focus from flowers to her eyes",
"wide establishing shot of field"
]
}
]
},
{
"sceneId": 6,
"time": "Afternoon",
"location": "Sunny street with pastel shops and flowers",
"outfit": "pastel yellow mini dress with white knee-high socks and sneakers",
"cuts": [
{
"cutId": 1,
"pose": "standing in middle of street",
"action": "spinning playfully with dress flaring",
"camera": [
"wide establishing shot of street",
"low angle focusing on dress spin",
"tracking dolly circling her",
"rear shot capturing spin from behind",
"slow zoom-in on smiling face"
]
},
{
"cutId": 2,
"pose": "walking along pastel shop windows",
"action": "running fingers along glass",
"camera": [
"side tracking shot at hip level",
"close-up on hand brushing glass",
"reflection shot from window",
"rear follow dolly with blurred people",
"wide shot capturing full storefront"
]
},
{
"cutId": 3,
"pose": "leaning against pastel wall",
"action": "making a heart shape with hands",
"camera": [
"tight close-up on hands forming heart",
"fish-eye wide close-up",
"side shot at 45°",
"POV as if receiving heart",
"zoom burst effect from wide to close"
]
},
{
"cutId": 4,
"pose": "sitting on curb",
"action": "tying shoelaces while smiling up",
"camera": [
"low angle close-up on hands and laces",
"rear over-shoulder capturing face",
"front medium shot with pastel background",
"wide shot with street perspective",
"handheld dolly-in to her smile"
]
},
{
"cutId": 5,
"pose": "holding ice cream",
"action": "taking playful bite and laughing",
"camera": [
"close-up on lips with ice cream",
"profile with blurred shops",
"rear shot with dripping ice cream",
"slow-motion front focus on laugh",
"wide shot with passing bicycles"
]
}
]
},
{
"sceneId": 7,
"time": "Afternoon",
"location": "Trendy café with modern pastel interior",
"outfit": "white blouse tucked into pleated skirt with sheer white stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting at table with chin on hands",
"action": "smiling softly while tilting head",
"camera": [
"front eye-level close-up",
"over-shoulder with coffee cup foreground",
"soft focus on eyes with blurred café",
"panning shot across table",
"low angle from tabletop upward"
]
},
{
"cutId": 2,
"pose": "standing by window",
"action": "writing playfully in notebook",
"camera": [
"profile with sunlight flare",
"close-up of pen moving on page",
"reflection in window glass",
"tracking dolly up from feet",
"medium shot with soft bokeh"
]
},
{
"cutId": 3,
"pose": "sitting sideways on chair",
"action": "taking sip of coffee",
"camera": [
"tight focus on lips with cup",
"medium framed by chair back",
"dutch angle for playful energy",
"slow dolly-in on sip",
"wide establishing café interior"
]
},
{
"cutId": 4,
"pose": "leaning on counter",
"action": "resting chin on hands dreamily",
"camera": [
"profile with pastel counter lights",
"close-up on dreamy eyes",
"rear shot capturing café depth",
"handheld tilt upward from arms",
"medium shot with blurred background"
]
},
{
"cutId": 5,
"pose": "standing at café entrance",
"action": "waving playfully toward camera",
"camera": [
"wide shot framing doorway",
"rear dolly out into street",
"tight close-up on waving hand",
"low angle capturing door sign",
"tracking shot circling wave"
]
}
]
},
{
"sceneId": 8,
"time": "Afternoon",
"location": "Hilltop path with light mist and mountain view",
"outfit": "lavender cardigan with flared pastel skirt and gray tights",
"cuts": [
{
"cutId": 1,
"pose": "walking up misty hill path",
"action": "holding skirt slightly while climbing",
"camera": [
"rear tracking shot on path",
"low angle capturing feet and mist",
"wide shot with valley below",
"side profile with wind blowing hair",
"drone reveal rising above hill"
]
},
{
"cutId": 2,
"pose": "standing at lookout",
"action": "stretching arms wide into mist",
"camera": [
"front wide shot with horizon",
"rear silhouette against misty valley",
"low angle with sky backdrop",
"close-up on hands spreading",
"slow dolly-in from side"
]
},
{
"cutId": 3,
"pose": "sitting on rock",
"action": "tying hair with calm expression",
"camera": [
"medium shot side profile",
"close-up on hands tying hair",
"rear shot framing valley mist",
"handheld zoom to her smile",
"wide establishing shot with mountains"
]
},
{
"cutId": 4,
"pose": "kneeling on grass",
"action": "picking wildflowers gently",
"camera": [
"macro on hand picking flowers",
"low angle with valley in blur",
"profile with mist behind",
"top-down on her hands",
"slow pan across field to her face"
]
},
{
"cutId": 5,
"pose": "walking down hill path",
"action": "looking back over shoulder smiling",
"camera": [
"rear tracking dolly with path",
"close-up on over-shoulder smile",
"wide shot with valley mist",
"side tracking with flowing skirt",
"drone pull-back revealing whole scene"
]
}
]
},
{
"sceneId": 9,
"time": "Night",
"location": "Balcony overlooking city lights",
"outfit": "elegant black mini dress with sheer black stockings and heels",
"cuts": [
{
"cutId": 1,
"pose": "standing at balcony railing",
"action": "gazing out over city lights",
"camera": [
"rear wide shot with city skyline",
"profile silhouette against glowing lights",
"close-up on hands gripping railing",
"low angle capturing her and the skyline",
"slow dolly-in from doorway"
]
},
{
"cutId": 2,
"pose": "leaning on railing with chin resting",
"action": "smiling softly at the horizon",
"camera": [
"side profile with blurred city bokeh",
"tight close-up on soft smile",
"rear tracking shot along railing",
"medium shot with glowing skyline",
"handheld tilt capturing candid mood"
]
},
{
"cutId": 3,
"pose": "sitting on balcony chair",
"action": "crossing legs elegantly while holding a glass",
"camera": [
"front eye-level medium shot",
"low angle focusing on legs and glass",
"profile silhouette against city",
"close-up on glass near lips",
"wide establishing balcony view"
]
},
{
"cutId": 4,
"pose": "standing near glass door",
"action": "touching window glass gently",
"camera": [
"rear shot with reflection in glass",
"side profile with neon reflection",
"close-up of hand on glass surface",
"soft focus tracking along her arm",
"wide shot framing city lights outside"
]
},
{
"cutId": 5,
"pose": "walking back inside",
"action": "glancing over shoulder toward city",
"camera": [
"rear tracking shot into room",
"close-up on over-shoulder look",
"wide establishing shot of balcony",
"low angle capturing dress movement",
"slow dolly-in on her eyes"
]
}
]
},
{
"sceneId": 10,
"time": "Night",
"location": "Garden with lanterns and candlelight",
"outfit": "flowy white long dress with lace stockings",
"cuts": [
{
"cutId": 1,
"pose": "walking along lantern-lit path",
"action": "holding a lantern gently in hand",
"camera": [
"wide shot with glowing lanterns",
"rear tracking with path perspective",
"close-up on lantern light in hand",
"profile with candle bokeh",
"drone shot rising above path"
]
},
{
"cutId": 2,
"pose": "kneeling by candle arrangement",
"action": "lighting one candle softly",
"camera": [
"macro on candle flame igniting",
"low angle capturing her face glow",
"rear shot with many candles blurred",
"side profile with flickering shadows",
"handheld close-up on gentle smile"
]
},
{
"cutId": 3,
"pose": "sitting at small garden table",
"action": "resting chin on hands dreamily",
"camera": [
"front close-up with lantern foreground",
"side profile framed by soft lights",
"wide shot showing whole garden",
"over-shoulder focusing on glowing eyes",
"slow dolly-in to serene smile"
]
},
{
"cutId": 4,
"pose": "standing beneath lantern tree",
"action": "raising arms as if embracing lights",
"camera": [
"low angle capturing lantern canopy",
"rear silhouette with glowing tree",
"profile with bokeh lights behind",
"wide establishing garden lights",
"tracking circle shot around her pose"
]
},
{
"cutId": 5,
"pose": "walking barefoot on grass",
"action": "spinning lightly with dress flowing",
"camera": [
"rear dolly capturing flowing fabric",
"low angle focusing on feet in grass",
"wide shot with lanterns in background",
"close-up on hair moving in spin",
"handheld pan following her spin"
]
}
]
},
{
"sceneId": 11,
"time": "Night",
"location": "Grand medieval-style ballroom lit by hundreds of candles with golden chandeliers and gothic arches",
"outfit": "elegant layered gown with multiple flowing fabric layers in soft pastel tones, paired with lace stockings and delicate shoes",
"appearance": "hair styled in loose curls with a jeweled tiara, candlelight reflecting on the layered dress",
"cuts": [
{
"cutId": 1,
"pose": "standing gracefully in center of hall with arms slightly lifted",
"action": "beginning a slow dance step, gazing downward softly",
"camera": [
"wide establishing shot of candlelit hall",
"low angle emphasizing layered gown",
"side profile with candles blurred in background",
"rear shot capturing the dress flowing behind",
"close-up on soft serene expression"
]
},
{
"cutId": 2,
"pose": "spinning lightly with gown flaring out",
"action": "smiling gracefully as layers swirl",
"camera": [
"front wide shot capturing gown layers in motion",
"low angle showing candles and fabric layers",
"rear shot with chandeliers glowing above",
"close-up on swirling gown fabric",
"handheld circular dolly around spin"
]
},
{
"cutId": 3,
"pose": "holding skirt edges delicately with both hands",
"action": "stepping forward slowly with elegance",
"camera": [
"medium shot focusing on skirt layers",
"profile capturing her careful step",
"low angle with candlelight flicker",
"rear shot with her shadow on marble floor",
"close-up on hands lifting fabric layers"
]
},
{
"cutId": 4,
"pose": "pausing mid-dance with one hand extended outward",
"action": "gazing toward candlelit balcony dreamily",
"camera": [
"front shot framing extended hand",
"side profile with glowing candles",
"rear wide shot capturing full hall depth",
"close-up on face with candlelight reflections",
"tracking dolly moving slowly toward her"
]
},
{
"cutId": 5,
"pose": "ending dance with a graceful curtsy",
"action": "bowing slightly with layered gown cascading",
"camera": [
"wide shot showing curtsy against hall arches",
"low angle emphasizing fabric folds",
"close-up on gown layers folding gracefully",
"rear shot with chandeliers above her",
"handheld tilt from gown to her smile"
]
}
]
},
{
"sceneId": 12,
"time": "Night",
"location": "Asian summer festival with hundreds of glowing lanterns floating into the night sky",
"outfit": "cute pink summer dress with light fabric and white sandals",
"appearance": "hair gently tied back with loose strands, warm glow of lantern light reflecting on her skin",
"cuts": [
{
"cutId": 1,
"pose": "standing among festival crowd holding a lantern",
"action": "smiling softly as she prepares to release it",
"camera": [
"front medium shot with lantern glowing in hands",
"close-up on her smile illuminated by warm light",
"side profile with lantern crowd blurred",
"rear shot with lanterns flying above",
"wide establishing shot of festival atmosphere"
]
},
{
"cutId": 2,
"pose": "lifting lantern upward with both hands",
"action": "watching it float away with hopeful expression",
"camera": [
"low angle capturing lantern rising above her",
"front wide shot with multiple lanterns",
"close-up on her face lit by lantern glow",
"rear silhouette shot against lantern sky",
"handheld tilt following lantern upward"
]
},
{
"cutId": 3,
"pose": "walking slowly through festival path",
"action": "gazing up at lantern-filled sky",
"camera": [
"wide shot with festival stalls glowing",
"rear tracking shot with lanterns overhead",
"profile with lantern light on her cheek",
"close-up on eyes reflecting lanterns",
"medium shot with soft bokeh background"
]
},
{
"cutId": 4,
"pose": "kneeling to help a child with lantern",
"action": "smiling warmly while adjusting the lantern",
"camera": [
"front medium shot of interaction",
"close-up on hands fixing lantern",
"side profile with glowing lanterns above",
"rear shot with childs lantern rising",
"wide establishing shot with crowd and lights"
]
},
{
"cutId": 5,
"pose": "standing still gazing upward",
"action": "holding hands together in small prayer as lanterns rise",
"camera": [
"low angle with sky full of lanterns",
"front shot focusing on gentle prayer",
"rear silhouette against glowing night sky",
"side profile capturing emotional look",
"drone pull-out showing her among crowd"
]
}
]
}
]
}

Binary file not shown.

View File

@ -0,0 +1,272 @@
{
"character": {
"bodyType": "slim and youthful",
"hairStyle": "long wavy hair with pastel blue and purple colors"
},
"scenes": [
{
"sceneId": 1,
"time": "Morning",
"location": "Cozy bedroom with pastel bedding and sunlight through curtains",
"outfit": "white oversized pajama shirt with thigh-high stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying on bed with head resting on hands",
"action": "smiling softly at the camera",
"camera": [
"overhead shot from above capturing sunlight on her hair",
"close-up of her face with shallow depth of field",
"side angle showing body stretched on bed",
"slow zoom-in from doorway",
"handheld camera wobble for intimate feeling"
]
},
{
"cutId": 2,
"pose": "stretching arms above head while sitting on bed",
"action": "yawning cutely with eyes half closed",
"camera": [
"medium shot from foot of bed",
"low angle from floor emphasizing legs",
"wide shot with window light flaring",
"tracking shot circling around her stretch",
"soft focus tilt-shift framing"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with playful look",
"action": "blowing a kiss to the camera",
"camera": [
"front close-up catching kiss in slow motion",
"wide shot with pastel background",
"camera tilt from below lips to eyes",
"360° pan around her kneeling pose",
"handheld push-in as she blows kiss"
]
}
]
},
{
"sceneId": 2,
"time": "Morning",
"location": "Bedroom near window with plants and sunlight",
"outfit": "light pastel camisole with short pleated skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting on window sill, legs slightly crossed",
"action": "looking outside then turning to smile",
"camera": [
"silhouette against sunlight",
"side profile with plants blurred in foreground",
"close-up of smile turning to camera",
"dolly-in from outside window glass",
"over-shoulder shot capturing view outside"
]
},
{
"cutId": 2,
"pose": "leaning against the wall, hands behind back",
"action": "giggling with shy expression",
"camera": [
"eye-level shot with wall texture visible",
"slight high angle to emphasize cuteness",
"medium close-up on giggle with tilt",
"rack focus between wall décor and her face",
"soft handheld sway left to right"
]
},
{
"cutId": 3,
"pose": "lying on floor with legs up against the wall",
"action": "kicking feet playfully while laughing",
"camera": [
"top-down view from ceiling",
"low angle from foot level",
"side shot capturing playful kicks",
"slow pan across her body",
"handheld camera zoom-in to laughter"
]
}
]
},
{
"sceneId": 3,
"time": "Afternoon",
"location": "Sunny street with pastel shops and flowers",
"outfit": "short pastel pink dress with white stockings and sneakers",
"cuts": [
{
"cutId": 1,
"pose": "standing with one hand on hip",
"action": "spinning gently in place",
"camera": [
"full body wide shot with pastel shops",
"low angle capturing dress swirl",
"tracking shot circling spin",
"handheld slow zoom-in to face",
"rear shot revealing spin from behind"
]
},
{
"cutId": 2,
"pose": "walking with small steps",
"action": "waving happily at the camera",
"camera": [
"tracking dolly shot in front",
"overhead drone shot of street",
"side follow shot at hip level",
"handheld jitter to mimic vlog",
"close-up on waving hand with face blurred"
]
},
{
"cutId": 3,
"pose": "leaning forward playfully",
"action": "making a heart shape with hands",
"camera": [
"tight close-up on hands forming heart",
"fish-eye wide close-up",
"side shot at 45°",
"POV shot as if receiving heart",
"zoom burst effect from wide to close"
]
}
]
},
{
"sceneId": 4,
"time": "Afternoon",
"location": "Trendy café with bright modern interior",
"outfit": "white blouse tucked into pleated skirt with stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting at a table with chin on hands",
"action": "smiling softly while tilting head",
"camera": [
"front eye-level close-up",
"soft focus on eyes, blurred background",
"over-shoulder shot of coffee cup",
"panning shot across table to her",
"low angle from table surface"
]
},
{
"cutId": 2,
"pose": "standing by window with crossed legs",
"action": "writing playfully in a small notebook",
"camera": [
"profile with sunlight flare",
"close-up of pen on paper then tilt to face",
"tracking shot from feet to head",
"reflection in window glass",
"medium shot with bokeh lights behind"
]
},
{
"cutId": 3,
"pose": "sitting sideways on chair with one leg up",
"action": "taking a sip from a cup with cute expression",
"camera": [
"tight focus on lips touching cup",
"medium shot framed by chair back",
"slight dutch angle for energy",
"slow dolly-in on playful sip",
"wide establishing shot of café"
]
}
]
},
{
"sceneId": 5,
"time": "Night",
"location": "Elegant ballroom with chandeliers",
"outfit": "sparkly silver mini dress with black stockings and heels",
"cuts": [
{
"cutId": 1,
"pose": "standing tall with one hand on waist",
"action": "turning slowly while smiling confidently",
"camera": [
"wide shot capturing chandelier",
"low angle emphasizing elegance",
"tracking dolly rotation",
"close-up of smile during turn",
"rear tracking shot revealing gown shimmer"
]
},
{
"cutId": 2,
"pose": "sitting gracefully on a velvet chair",
"action": "crossing legs elegantly",
"camera": [
"medium close-up from side",
"top-down angle showing chair texture",
"front focus on crossed legs",
"soft rack focus from chair to her face",
"panning shot circling chair"
]
},
{
"cutId": 3,
"pose": "leaning on railing with dreamy look",
"action": "gazing at the chandelier lights",
"camera": [
"over-shoulder shot of chandelier view",
"profile silhouette with golden backlight",
"wide shot capturing ballroom depth",
"handheld tilt up from railing to face",
"slow dolly-out revealing emptiness of hall"
]
}
]
},
{
"sceneId": 6,
"time": "Night",
"location": "Luxurious bedroom with soft golden lighting",
"outfit": "black lace camisole with mini skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying sideways on bed with legs slightly bent",
"action": "looking at the camera with sultry eyes",
"camera": [
"close-up on eyes with blurred background",
"tracking shot along legs up to face",
"overhead soft focus",
"low angle from bed surface",
"handheld intimate pan across body"
]
},
{
"cutId": 2,
"pose": "sitting at vanity table",
"action": "putting on earrings slowly",
"camera": [
"mirror reflection focus",
"close-up on hands adjusting earrings",
"profile side shot with warm glow",
"over-shoulder shot including vanity lights",
"slow push-in from doorway"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with arched back",
"action": "running hand through hair with sensual expression",
"camera": [
"rear shot with back arch emphasized",
"medium close-up focusing on hair movement",
"low angle capturing curves",
"soft focus candlelight bokeh",
"circling dolly shot for dramatic effect"
]
}
]
}
]
}

View File

@ -0,0 +1,259 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { logger } from '../../lib/logger';
import { callOpenAI } from '../../lib/openai';
import { generateVideo } from '../../lib/video-generator';
dotenv.config();
type Size = { width: number; height: number };
interface MusicSpotCharacter {
bodyType: string;
hairStyle: string;
}
interface MusicSpotCut {
cutId: number;
pose: string;
action: string;
camera?: string[]; // list of camera variants per cut
}
interface MusicSpotScene {
sceneId: number;
time: string;
location: string;
outfit: string;
cuts: MusicSpotCut[];
}
interface MusicSpotConfig {
character: MusicSpotCharacter;
scenes: MusicSpotScene[];
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 1280, height: 720 };
const FOLDER = process.argv[2] || process.env.MUSICSPOT_FOLDER || 'infinitydance';
const FOLDER_SAFE = FOLDER.replace(/[/\\?%*:|"<>]/g, '_');
const GENERATED_DIR = path.resolve('generated');
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},
/*
{
name: 'SERVER2',
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
},*/
]
.filter((s) => !!s.baseUrl && !!s.outputDir)
.map((s) => ({
...s,
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
function buildVideoPromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string
): string {
return `
Return exactly one JSON object and nothing else: { "videoPrompt": "..." }.
Write "videoPrompt" in 100140 words. Present tense. Concrete, simple sentences.
HARD RULES:
- One continuous 8-second shot (oner). No edits.
- Fixed location and general vantage; maintain spatial continuity.
- No zooms, no rack zoom, no smash/push-in, no cuts, no transitions, no "meanwhile".
- Camera motion: at most a slight pan/tilt or subtle dolly within 1 meter.
- Keep framing consistent (vertical 720x1280). Avoid technical brand names or lens jargon.
Incorporate the following camera intention: "${cameraIntent}".
If it conflicts with HARD RULES (e.g., zoom, push-in, extreme moves), reinterpret it into a subtle, compliant motion (e.g., gentle glide, slight pan/tilt) while preserving the creative intent.
Describe:
1) Main action: ${cut.action}
2) Pose/composition: ${cut.pose}
3) Scene/time/location/outfit: ${scene.time}; ${scene.location}; outfit: ${scene.outfit}
4) Lighting/mood/style coherent with the character: ${character.bodyType}; hair: ${character.hairStyle}
Prohibited (case-insensitive): cut, cuts, cutting, quick cut, insert, close-up, extreme close-up, zoom, zooming, push-in, pull-out, whip, switch angle, change angle, montage, cross-cut, smash cut, transition, meanwhile, later.
Only respond with JSON.
`.trim();
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function getVideoPromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.videoPrompt || res?.video_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return videoPrompt JSON.');
}
return prompt.trim();
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function copyImageToAllServerInputs(servers: Server[], localGeneratedImagePath: string): Promise<string> {
const fileName = path.basename(localGeneratedImagePath);
for (const s of servers) {
if (!s.inputDir) continue;
const dest = path.join(s.inputDir, fileName);
try {
await fs.copyFile(localGeneratedImagePath, dest);
logger.debug(`Copied ${fileName} to ${s.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy ${fileName} to ${s.name} input: ${err}`);
}
}
return fileName; // return the name used for Comfy workflows
}
async function fileExists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function main() {
try {
await ensureDirs();
// Load scenes.json
const configRaw = await fs.readFile(path.resolve(`src/musicspot_generator/${FOLDER}/scenes.json`), 'utf-8');
const cfg: MusicSpotConfig = JSON.parse(configRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Generate videos only, based on images already present in ./generated
let videoTaskIndex = 0;
for (const scene of cfg.scenes) {
logger.info(`=== Scene ${scene.sceneId}: Video generation start ===`);
for (const cut of scene.cuts) {
const cameraVariants =
Array.isArray(cut.camera) && cut.camera.length > 0
? cut.camera
: ['eye-level medium shot', 'slight left 30°', 'slight right 30°', 'slight high angle', 'slight low angle'];
for (let camIdx = 0; camIdx < cameraVariants.length; camIdx++) {
const cameraIntent = cameraVariants[camIdx];
const variantIndex = camIdx + 1;
const imgFileName = `${FOLDER_SAFE}_musicspot_s${scene.sceneId}_c${cut.cutId}_v${variantIndex}.png`;
const imgPath = path.join(GENERATED_DIR, imgFileName);
// Only proceed if image exists
const hasImage = await fileExists(imgPath);
if (!hasImage) {
logger.warn(`Skipping video: source image not found: ${imgPath}`);
continue;
}
const videoFileName = imgFileName.replace(/\.png$/i, '.mp4');
const videoOutPath = path.join(GENERATED_DIR, videoFileName);
// Skip if video already
const hasVideo = await fileExists(videoOutPath);
if (hasVideo) {
logger.info(`Video already exists, skipping: ${videoOutPath}`);
continue;
}
// 1) Generate video prompt for this camera
logger.info(
`Scene ${scene.sceneId} - Cut ${cut.cutId} - Cam${variantIndex}: generating video prompt from image ${imgFileName}...`
);
const vidPromptReq = buildVideoPromptRequest(cfg.character, scene, cut, cameraIntent);
let videoPrompt: string;
try {
videoPrompt = await getVideoPromptFromOpenAI(vidPromptReq);
} catch (err) {
logger.error(`OpenAI video prompt failed for ${imgFileName}: ${err}`);
continue;
}
// 2) Copy the base image to every server's input folder
const imageFileNameForComfy = await copyImageToAllServerInputs(servers, imgPath);
// 3) Generate video on a chosen server (round-robin)
const serverForVideo = pickServer(servers, videoTaskIndex++);
logger.info(`Generating video (${videoFileName}) on ${serverForVideo.name} using ${imageFileNameForComfy}...`);
try {
const videoPath = await generateVideo(
videoPrompt,
imageFileNameForComfy,
videoFileName,
serverForVideo.baseUrl!,
serverForVideo.outputDir!,
DEFAULT_SIZE,
false,
false
);
await sleep(10000); // wait a bit for file system to settle
logger.info(`Video generated: ${videoPath}`);
} catch (err) {
logger.error(`Video generation failed (${videoFileName}) on ${serverForVideo.name}: ${err}`);
}
}
}
logger.info(`=== Scene ${scene.sceneId}: Video generation complete ===`);
}
logger.info('Video generation for all scenes completed.');
} catch (err) {
logger.error('Fatal error in music spot video generator:', err);
}
}
main().catch((err) => {
logger.error('Unhandled error:', err);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,834 @@
{
"character": {
"bodyType": "average",
"hairStyle": "long blonde hair tied in a man bun, white skin, blue eyes, beard"
},
"scenes": [
{
"sceneId": 1,
"season": "Autumn",
"time": "Evening",
"location": "Zagreb skyline viewpoint with light fog and warm city lights",
"outfit": "dark gray adidas joggers with white side stripes, black heavyweight hoodie (hood down), light quilted autumn jacket in charcoal, white adidas sneakers slightly scuffed, black knit beanie",
"cuts": [
{
"cutId": 1,
"pose": "standing at viewpoint railing",
"action": "looking over city while exhaling misty breath",
"camera": [
"fullbody shot with skyline and fog glow",
"closeup on eyes and misty breath",
"back shot framed by city lights"
]
},
{
"cutId": 2,
"pose": "hands in pockets",
"action": "nodding head slowly to beat",
"camera": [
"fullbody shot silhouetted against skyline",
"closeup on subtle head nod and beard",
"side shot with drifting fog"
]
},
{
"cutId": 3,
"pose": "feet near railing edge",
"action": "dancing: tight heel-toe shuffle forward and back, ankles loose, arms swinging low in relaxed groove",
"camera": [
"fullbody shot wide skyline backdrop",
"closeup on footwork over wet stone",
"side shot tracking along railing"
]
},
{
"cutId": 4,
"pose": "one arm lifted",
"action": "dancing: step-touch left/right with rolling shoulders and head bobs on the snare",
"camera": [
"fullbody shot low angle, lights behind",
"closeup on rolling shoulders and hoodie fabric",
"back shot into glowing fog"
]
},
{
"cutId": 5,
"pose": "leaning against railing",
"action": "staring straight into lens with confident half-smile",
"camera": [
"fullbody shot framed by rail",
"closeup on confident stare",
"side shot with fog drift"
]
}
]
},
{
"sceneId": 2,
"season": "Autumn",
"time": "Day",
"location": "Inside Zagreb blue tram, sunlight streaks through windows",
"outfit": "mid-blue jeans, white cotton t-shirt under light gray zip hoodie, navy bomber jacket, clean white adidas sneakers, simple black wristwatch",
"cuts": [
{
"cutId": 1,
"pose": "sitting by window",
"action": "writing lyrics in notebook as city blurs past",
"camera": [
"fullbody shot seated by window",
"closeup on pen and notebook",
"side shot with city motion streaks"
]
},
{
"cutId": 2,
"pose": "standing near door holding rail",
"action": "dancing: rail-hold groove with shoulder ticks and toe taps in sync with tram sway",
"camera": [
"fullbody shot vertical down tram aisle",
"closeup on toe taps on rubber floor",
"side shot from opposite pole"
]
},
{
"cutId": 3,
"pose": "leaning on glass",
"action": "fogging window with breath then drawing a small heart",
"camera": [
"fullbody shot in window bay",
"closeup on fingertip tracing heart",
"side shot with sunlight flare"
]
},
{
"cutId": 4,
"pose": "feet apart, both hands on strap",
"action": "dancing: micro two-step in place with chest pops on backbeat",
"camera": [
"fullbody shot in aisle",
"closeup on chest pops through hoodie",
"back shot down toward exit door"
]
},
{
"cutId": 5,
"pose": "stepping off tram",
"action": "placing foot on wet pavement and glancing up",
"camera": [
"fullbody shot exiting with tram behind",
"closeup on sneaker hitting puddle",
"side shot tracking walk away"
]
}
]
},
{
"sceneId": 3,
"season": "Autumn",
"time": "Morning",
"location": "Dolac market with red umbrellas, crisp air",
"outfit": "black adidas joggers, cream knit t-shirt under a denim trucker jacket, white adidas sneakers with gray socks, fingerless knit gloves",
"cuts": [
{
"cutId": 1,
"pose": "walking through market",
"action": "nodding to vendors and smiling",
"camera": [
"fullbody shot weaving among umbrellas",
"closeup on smile and beard",
"side shot with shoppers blurred"
]
},
{
"cutId": 2,
"pose": "open space between stalls",
"action": "dancing: pivot-spin on heel, one arm slicing air, stopping on beat",
"camera": [
"fullbody shot centered under umbrellas",
"closeup on pivoting sneaker",
"back shot with umbrellas radiating"
]
},
{
"cutId": 3,
"pose": "holding an apple",
"action": "taking a bite and chuckling",
"camera": [
"fullbody shot framed by produce colors",
"closeup on bite and beard",
"side shot with vendor in bokeh"
]
},
{
"cutId": 4,
"pose": "near stall edge",
"action": "dancing: side-step bounce with finger snaps and quick shoulder check",
"camera": [
"fullbody shot on cobblestone",
"closeup on finger snaps",
"side shot past crates of apples"
]
},
{
"cutId": 5,
"pose": "sitting on bench",
"action": "exhaling a long misty breath up to sky",
"camera": [
"fullbody shot bench among umbrellas",
"closeup on breath plume",
"back shot to morning light"
]
}
]
},
{
"sceneId": 4,
"season": "Autumn",
"time": "Afternoon",
"location": "Graffiti alley glowing with neon tubes and hanging fog",
"outfit": "charcoal adidas joggers, navy heavyweight hoodie layered under a black denim jacket, white adidas sneakers worn at toes, charcoal beanie, silver chain",
"cuts": [
{
"cutId": 1,
"pose": "line with two friends",
"action": "dancing: synchronized stomp-stomp-slide combo with shoulder hits (together)",
"camera": [
"fullbody shot of trio in wide alley",
"closeup on synchronized sneakers",
"side shot panning across the line"
]
},
{
"cutId": 2,
"pose": "center of small circle",
"action": "dancing: knee spin to floor to knee up while friends clap and hype (together focus on him)",
"camera": [
"fullbody shot near ground level",
"closeup on balancing palm and sleeve",
"back shot showing friends clapping"
]
},
{
"cutId": 3,
"pose": "leaning on wall",
"action": "rapping directly to camera, neon flicker behind",
"camera": [
"fullbody shot against graffiti panel",
"closeup on lips and breath in cold air",
"side shot with neon reflections"
]
},
{
"cutId": 4,
"pose": "shoulder to shoulder with friends",
"action": "dancing: side-step shuffle with hand waves and head whips (together)",
"camera": [
"fullbody shot three-wide groove",
"closeup on hand waves across frame",
"back shot down glowing alley"
]
},
{
"cutId": 5,
"pose": "arms stretched open",
"action": "looking up as fog lifts around lights",
"camera": [
"fullbody shot silhouette in neon haze",
"closeup on eyes lit by tubes",
"side shot past a painted mural"
]
}
]
},
{
"sceneId": 5,
"season": "Autumn",
"time": "Evening",
"location": "Sava riverbank at golden hour with low mist",
"outfit": "mid-wash jeans, black long-sleeve tee under olive lightweight parka, white adidas sneakers with darker laces, gray knit scarf",
"cuts": [
{
"cutId": 1,
"pose": "walking riverside",
"action": "dancing: loose arm swing with heel peels and forward glide",
"camera": [
"fullbody shot along river path",
"closeup on heel peel on damp gravel",
"side shot with river reflections"
]
},
{
"cutId": 2,
"pose": "hands in parka pockets",
"action": "gazing at sun reflecting on water",
"camera": [
"fullbody shot silhouette at bank",
"closeup on eyes catching gold",
"back shot toward low sun"
]
},
{
"cutId": 3,
"pose": "standing by railing",
"action": "tapping rhythm on rail with knuckles",
"camera": [
"fullbody shot framed by rail lines",
"closeup on tapping knuckles",
"side shot with mist ribboning"
]
},
{
"cutId": 4,
"pose": "open space at water edge",
"action": "dancing: slide-to-cross step with chest pop accent and arm sweep",
"camera": [
"fullbody shot river behind",
"closeup on chest pop under tee",
"side shot tracking along water"
]
},
{
"cutId": 5,
"pose": "head tilted back",
"action": "breathing out slowly, eyes closed",
"camera": [
"fullbody shot with fading sun",
"closeup on breath plume dispersing",
"back shot to amber sky"
]
}
]
},
{
"sceneId": 6,
"season": "Autumn",
"time": "Night",
"location": "Ban Jelačić Square with neon ads and light fog",
"outfit": "black adidas joggers, gray t-shirt, slate bomber jacket, white adidas sneakers, fingerless gloves",
"cuts": [
{
"cutId": 1,
"pose": "group line facing camera",
"action": "dancing: bounce-bounce-step pattern with knee lifts in unison (together)",
"camera": [
"fullbody shot of group centered in square",
"closeup on synchronized knees",
"side shot with neon bokeh"
]
},
{
"cutId": 2,
"pose": "front of dance circle",
"action": "dancing: spin in, arms open, stop on beat with chest hit (together feature)",
"camera": [
"fullbody shot in circle opening",
"closeup on arm extension and jacket fold",
"back shot toward statue and lights"
]
},
{
"cutId": 3,
"pose": "friends behind clapping",
"action": "rapping at lens with fog swirling",
"camera": [
"fullbody shot square tiles reflecting",
"closeup on lips and beard",
"side shot past cheering friends"
]
},
{
"cutId": 4,
"pose": "two friends flanking",
"action": "dancing: traveling shuffle diagonally with toe digs and head tilts (together)",
"camera": [
"fullbody shot three-across traveling",
"closeup on toe digs kicking sparks of water",
"back shot crossing square"
]
},
{
"cutId": 5,
"pose": "hands on hips",
"action": "smiling up at neon glow",
"camera": [
"fullbody shot slight low angle",
"closeup on smiling eyes",
"side shot with tram passing blur"
]
}
]
},
{
"sceneId": 7,
"season": "Autumn",
"time": "Day",
"location": "Corner café with fogged windows and warm Edison bulbs",
"outfit": "dark jeans, soft oatmeal t-shirt, forest-green hoodie under tan canvas jacket, white adidas sneakers, wool cap",
"cuts": [
{
"cutId": 1,
"pose": "seated with two friends at table",
"action": "dancing: table-top beat — finger drumming while shoulders groove (together)",
"camera": [
"fullbody shot across small table",
"closeup on finger rhythms on wood",
"side shot with fogged window"
]
},
{
"cutId": 2,
"pose": "standing by chair facing friend",
"action": "dancing: mirrored mini two-step and clap on two (together)",
"camera": [
"fullbody shot of paired groove",
"closeup on clapping hands",
"back shot toward bar lights"
]
},
{
"cutId": 3,
"pose": "holding coffee cup",
"action": "blowing steam off cup and smirking",
"camera": [
"fullbody shot seated relaxed",
"closeup on steam curling",
"side shot with bulbs in bokeh"
]
},
{
"cutId": 4,
"pose": "friends around hyping",
"action": "dancing: quick spin ending in finger point to camera (together energy)",
"camera": [
"fullbody shot between tables",
"closeup on finger point and grin",
"back shot past other patrons"
]
},
{
"cutId": 5,
"pose": "leaning back in chair",
"action": "exhaling lightly, calm confidence",
"camera": [
"fullbody shot reclined posture",
"closeup on relaxed eyes",
"side shot across window condensation"
]
}
]
},
{
"sceneId": 8,
"season": "Autumn",
"time": "Evening",
"location": "Old Zagreb apartment balcony with laundry lines and city fog",
"outfit": "black adidas joggers, slate long-sleeve t-shirt under navy quilted vest, white adidas sneakers, knit scarf",
"cuts": [
{
"cutId": 1,
"pose": "leaning on rail under warm bulb",
"action": "writing lyric line in notebook",
"camera": [
"fullbody shot balcony edge",
"closeup on pen and paper",
"side shot past swaying laundry"
]
},
{
"cutId": 2,
"pose": "hands lifted",
"action": "dancing: shoulder rolls with slow side travel, wrists loose",
"camera": [
"fullbody shot city behind",
"closeup on rolling shoulders",
"back shot toward foggy rooftops"
]
},
{
"cutId": 3,
"pose": "center balcony tile",
"action": "dancing: foot slide-cross with quick head nod accents",
"camera": [
"fullbody shot top-down slight",
"closeup on sliding sneakers on tiles",
"side shot along rail"
]
},
{
"cutId": 4,
"pose": "headphones on",
"action": "nodding in time, eyes closed",
"camera": [
"fullbody shot under balcony lamp",
"closeup on serene face and beard",
"back shot to dim streetlights"
]
},
{
"cutId": 5,
"pose": "sitting on chair",
"action": "long breath into cold night air",
"camera": [
"fullbody shot with chair legs angled",
"closeup on breath plume",
"side shot from doorway"
]
}
]
},
{
"sceneId": 9,
"season": "Autumn",
"time": "Day",
"location": "Neighborhood street football court, leaves on ground",
"outfit": "navy tracksuit top, black adidas joggers, white adidas sneakers with turf dust, simple beanie",
"cuts": [
{
"cutId": 1,
"pose": "triangle with friends",
"action": "passing football quickly between feet (together)",
"camera": [
"fullbody shot wide half-court",
"closeup on ball taps and passes",
"side shot through metal fence"
]
},
{
"cutId": 2,
"pose": "center circle",
"action": "dancing: soccer-shuffle — toe taps on ball with hip sway (together)",
"camera": [
"fullbody shot center circle",
"closeup on rapid toe taps",
"back shot with friends cheering"
]
},
{
"cutId": 3,
"pose": "near goalpost",
"action": "dancing: step-over fake into spin and pose (solo flair)",
"camera": [
"fullbody shot near net",
"closeup on foot feint over ball",
"side shot across goal mesh"
]
},
{
"cutId": 4,
"pose": "leaning on post",
"action": "chuckling at camera, breath visible",
"camera": [
"fullbody shot framed by net",
"closeup on smile and breath",
"back shot to autumn trees"
]
},
{
"cutId": 5,
"pose": "group line on sideline",
"action": "dancing: circle footwork — alternating shuffles in and out (together)",
"camera": [
"fullbody shot line routine",
"closeup on synchronized steps",
"side shot along chalk line"
]
}
]
},
{
"sceneId": 10,
"season": "Autumn",
"time": "Night",
"location": "Rainy cobblestone street with neon reflections",
"outfit": "black jeans, charcoal hoodie with hood up, water-resistant black shell jacket, white adidas sneakers wet with rain",
"cuts": [
{
"cutId": 1,
"pose": "mid-street",
"action": "kicking puddle to splash arcs of neon water",
"camera": [
"fullbody shot centered on reflections",
"closeup on splash around sneaker",
"back shot with neon signs"
]
},
{
"cutId": 2,
"pose": "feet planted wide",
"action": "dancing: chest pulse on beat with slow head roll, rain dripping off brim",
"camera": [
"fullbody shot silhouette in rain",
"closeup on chest pulse through hoodie",
"side shot with water streaks"
]
},
{
"cutId": 3,
"pose": "arms open",
"action": "letting rain hit face, eyes closed",
"camera": [
"fullbody shot T-pose slight",
"closeup on raindrops on beard",
"back shot with street glow"
]
},
{
"cutId": 4,
"pose": "pivot foot ready",
"action": "dancing: 360° rain spin with heel skid and jacket flare",
"camera": [
"fullbody shot tracking spin",
"closeup on heel skid and spray",
"side shot following arc"
]
},
{
"cutId": 5,
"pose": "hood low",
"action": "staring into lens, breath steaming",
"camera": [
"fullbody shot still in rain",
"closeup on intense eyes",
"back shot with wet cobbles"
]
}
]
},
{
"sceneId": 11,
"season": "Autumn",
"time": "Morning",
"location": "Zagreb Cathedral steps in pale fog",
"outfit": "dark jeans, black crew tee, gray wool overcoat, white adidas sneakers, knit gloves",
"cuts": [
{
"cutId": 1,
"pose": "sitting mid-steps",
"action": "writing lyrics steadily",
"camera": [
"fullbody shot wide steps and spires",
"closeup on pen strokes",
"side shot with drifting fog"
]
},
{
"cutId": 2,
"pose": "standing arms open",
"action": "exhaling long breath to sky",
"camera": [
"fullbody shot facing façade",
"closeup on breath plume",
"back shot to gothic towers"
]
},
{
"cutId": 3,
"pose": "cleared area on steps",
"action": "dancing: step-tap pattern up two steps, back down, snap on beat",
"camera": [
"fullbody shot across steps",
"closeup on step taps",
"side shot along handrail"
]
},
{
"cutId": 4,
"pose": "headphones on",
"action": "slow nod timing to kick and snare",
"camera": [
"fullbody shot centered on landing",
"closeup on nod and beard",
"back shot to open square"
]
},
{
"cutId": 5,
"pose": "hands in coat pockets",
"action": "looking up the tower silently",
"camera": [
"fullbody shot low angle",
"closeup on upward gaze",
"side shot with stone texture"
]
}
]
},
{
"sceneId": 12,
"season": "Autumn",
"time": "Night",
"location": "Lantern-lit garden path with candle clusters and light ground fog",
"outfit": "black adidas joggers, charcoal long-sleeve tee, midnight blue bomber, white adidas sneakers catching candle glints",
"cuts": [
{
"cutId": 1,
"pose": "walking along lantern path",
"action": "lifting a lantern and holding it forward",
"camera": [
"fullbody shot through lantern aisle",
"closeup on lantern glow on face",
"back shot with lantern trail"
]
},
{
"cutId": 2,
"pose": "small clearing",
"action": "dancing: lantern in left hand, slow side-glide with soft wrist circles",
"camera": [
"fullbody shot framed by candles",
"closeup on circling wrist and flame",
"side shot across floating fog"
]
},
{
"cutId": 3,
"pose": "at small garden table",
"action": "resting chin on hands, dreamy stare",
"camera": [
"fullbody shot seated with lantern foreground",
"closeup on gentle eyes",
"back shot to candle clusters"
]
},
{
"cutId": 4,
"pose": "beneath hanging lights",
"action": "dancing: slow turn with arm rise, ending in still pose under lights",
"camera": [
"fullbody shot low angle to lights",
"closeup on raised palm",
"side shot passing candle bokeh"
]
},
{
"cutId": 5,
"pose": "bare path",
"action": "spinning lightly as jacket hem flows",
"camera": [
"fullbody shot tracking spin",
"closeup on jacket swirl",
"back shot retreating down path"
]
}
]
},
{
"sceneId": 13,
"season": "Autumn",
"time": "Evening",
"location": "Warm recording studio with wooden panels, microphone on stand with pop filter, soft amber lamps",
"outfit": "charcoal adidas joggers, black heavyweight t-shirt, dark zip hoodie open at chest, studio headphones, white adidas sneakers",
"cuts": [
{
"cutId": 1,
"pose": "standing at mic, one hand on headphones",
"action": "singing through pop filter with eyes closed",
"camera": [
"fullbody shot booth and panels",
"closeup on lips behind pop filter fabric",
"side shot across mic arm"
]
},
{
"cutId": 2,
"pose": "one hand gripping mic stand",
"action": "dancing: subtle knee bounce and torso sway while holding pitch",
"camera": [
"fullbody shot between panels",
"closeup on torso sway and hoodie folds",
"back shot from behind pop filter"
]
},
{
"cutId": 3,
"pose": "leaning slightly toward mic",
"action": "soft smile between lines, breath visible",
"camera": [
"fullbody shot centered on stand",
"closeup on smile and beard texture",
"side shot with amber lamp flare"
]
},
{
"cutId": 4,
"pose": "adjusting headphones",
"action": "dancing: micro step-touch in place with shoulder ticks as chorus hits",
"camera": [
"fullbody shot framed by acoustic panels",
"closeup on headphone cup and hand",
"back shot over shoulder into booth glass"
]
},
{
"cutId": 5,
"pose": "stool behind mic",
"action": "holding lyric sheet steady, one line delivered",
"camera": [
"fullbody shot seated by stand",
"closeup on lyric sheet print",
"side shot past pop filter ring"
]
}
]
},
{
"sceneId": 14,
"season": "Autumn",
"time": "Day",
"location": "Minimalist white studio booth, diffused panels, microphone with pop filter",
"outfit": "light gray adidas joggers, crisp white t-shirt, soft white zip hoodie, clean white adidas sneakers, over-ear studio headphones",
"cuts": [
{
"cutId": 1,
"pose": "sitting on white stool at mic",
"action": "singing gently into pop filter with steady breath",
"camera": [
"fullbody shot bright white booth",
"closeup on mouth behind pop fabric",
"side shot along mic arm and cable"
]
},
{
"cutId": 2,
"pose": "standing, lyric sheet in left hand",
"action": "dancing: soft sway with sheet hand marking the beat while delivering a phrase",
"camera": [
"fullbody shot against white wall",
"closeup on sheet hand pulsing beat",
"back shot to frosted panel glow"
]
},
{
"cutId": 3,
"pose": "head slightly tilted",
"action": "smiling at the take, exhale visible in cool booth",
"camera": [
"fullbody shot centered in booth",
"closeup on gentle smile",
"side shot with panel edge parallax"
]
},
{
"cutId": 4,
"pose": "hands lightly on headphones",
"action": "dancing: tiny step-rock left-right, shoulders ticking to metronome",
"camera": [
"fullbody shot with mic foreground",
"closeup on headphone cushions and fingers",
"back shot over shoulder to pop filter"
]
},
{
"cutId": 5,
"pose": "one hand on mic stand",
"action": "holding final note clean through pop filter",
"camera": [
"fullbody shot with stand and cable",
"closeup on pop filter mesh catching light",
"side shot from booth door"
]
}
]
}
]
}

Binary file not shown.

View File

@ -0,0 +1,78 @@
import * as fs from 'fs';
import * as path from 'path';
import { convertImageWithFile } from '../../lib/image-converter';
import dotenv from 'dotenv';
dotenv.config();
const girlDir = path.join(__dirname, '../../../input/girl');
const monsterDir = path.join(__dirname, '../../../input/monster');
const outputDir = path.join(__dirname, '../../../generated');
const prompt = "只提取图1中的女生把她放在图2的怪物之间。";
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const comfyBaseUrl = process.env.SERVER2_COMFY_BASE_URL;
const comfyOutputDir = process.env.SERVER2_COMFY_OUTPUT_DIR;
if (!comfyBaseUrl || !comfyOutputDir) {
console.error("Please define SERVER1_COMFY_BASE_URL and SERVER1_COMFY_OUTPUT_DIR in your .env file");
process.exit(1);
}
const comfyInputDir = comfyOutputDir.replace("output", "input");
if (!fs.existsSync(comfyInputDir)) {
fs.mkdirSync(comfyInputDir, { recursive: true });
}
async function combineImages() {
while (true) {
try {
const girlImages = fs.readdirSync(girlDir).filter(file => /\.(jpg|jpeg|png)$/i.test(file));
const monsterImages = fs.readdirSync(monsterDir).filter(file => /\.(jpg|jpeg|png)$/i.test(file));
if (girlImages.length === 0 || monsterImages.length === 0) {
console.log('Input directories are empty. Waiting...');
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}
const randomGirlImage = girlImages[Math.floor(Math.random() * girlImages.length)];
const randomMonsterImage = monsterImages[Math.floor(Math.random() * monsterImages.length)];
const image1Path = path.join(girlDir, randomGirlImage);
const image2Path = path.join(monsterDir, randomMonsterImage);
// Copy files to comfy input directory
const destImage1Path = path.join(comfyInputDir, randomGirlImage);
const destImage2Path = path.join(comfyInputDir, randomMonsterImage);
fs.copyFileSync(image1Path, destImage1Path);
fs.copyFileSync(image2Path, destImage2Path);
console.log(`Combining ${randomGirlImage} and ${randomMonsterImage}`);
const generatedFilePath = await convertImageWithFile(prompt, randomGirlImage, randomMonsterImage, comfyBaseUrl!, comfyOutputDir!);
if (generatedFilePath && fs.existsSync(generatedFilePath)) {
const timestamp = new Date().getTime();
const newFileName = `combined_${timestamp}.png`;
const newFilePath = path.join(outputDir, newFileName);
fs.renameSync(generatedFilePath, newFilePath);
console.log(`Renamed generated file to ${newFilePath}`);
} else {
console.log("Failed to generate or find the image file.");
}
} catch (error) {
console.error('An error occurred:', error);
}
// Wait for a bit before the next iteration
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
combineImages();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -0,0 +1,72 @@
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { generateImage } from '../../lib/image-generator-style-faceswap';
import { logger } from '../../lib/logger';
import dotenv from 'dotenv';
dotenv.config();
const scenesFilePath = path.resolve(process.cwd(), 'src/musicspot_generator/v2/scenes.json');
const faceFilePath = path.resolve(process.cwd(), 'src/musicspot_generator/v2/face.png');
const GENERATED_DIR = path.resolve('generated');
const DEFAULT_SIZE = { width: 1280, height: 720 };
interface Scene {
scene: string;
imagePrompt: {
description: string,
style: string,
lighting: string,
outfit: string,
location: string,
poses: string,
angle: string,
};
videoPromp: string;
baseImagePath: string;
}
const COMFY_BASE_URL = process.env.COMFY_BASE_URL;
const COMFY_OUTPUT_DIR = process.env.COMFY_OUTPUT_DIR;
async function generatePhotos() {
if (!COMFY_BASE_URL || !COMFY_OUTPUT_DIR) {
throw new Error('COMFY_BASE_URL or COMFY_OUTPUT_DIR is not defined in the .env file');
}
const scenesFileContent = fs.readFileSync(scenesFilePath, 'utf-8');
const scenesData: { scenes: Scene[] } = JSON.parse(scenesFileContent);
for (const scene of scenesData.scenes) {
const hash = crypto.createHash('sha256').update(scene.baseImagePath).digest('hex');
const imgFileName = `${hash}.png`;
const outputFilePath = path.join(GENERATED_DIR, imgFileName);
if (fs.existsSync(outputFilePath)) {
logger.info(`Skipping already generated photo for: ${scene.baseImagePath}`);
continue;
}
logger.info(`Generating photo for: ${scene.baseImagePath}`);
try {
await generateImage(
`Scary realistic photo, ${scene.imagePrompt.location},${scene.imagePrompt.angle},${scene.imagePrompt.lighting},${scene.imagePrompt.outfit}`,
faceFilePath,
scene.baseImagePath,
imgFileName,
COMFY_BASE_URL,
COMFY_OUTPUT_DIR,
DEFAULT_SIZE
);
logger.info(`Successfully generated photo: ${imgFileName}`);
} catch (error) {
logger.error(`Error generating photo for scene ${scene.scene}:`, error);
}
}
}
generatePhotos().catch(error => {
logger.error('An unexpected error occurred:', error);
});

View File

@ -0,0 +1,87 @@
import fs from 'fs';
import path from 'path';
import { callLMStudioAPIWithFile } from '../../lib/lmstudio';
import { logger } from '../../lib/logger';
const promptInstructions = `
Video prompt: No slowmotion, Be creative and generate gengle action scene.
`;
6
const inputDir = path.resolve(process.cwd(), 'input/static');
const outputFilePath = path.resolve(process.cwd(), 'src/musicspot_generator/v2/scenes.json');
interface Scene {
scene: string;
imagePrompt: string;
videoPromp: string;
baseImagePath: string;
}
async function processImages() {
const imageFiles = fs.readdirSync(inputDir).filter(file => /\.(png|jpg|jpeg)$/i.test(file));
let scenes: { scenes: Scene[] } = { scenes: [] };
if (fs.existsSync(outputFilePath)) {
const fileContent = fs.readFileSync(outputFilePath, 'utf-8');
if (fileContent) {
scenes = JSON.parse(fileContent);
}
}
for (const imageFile of imageFiles) {
const imagePath = path.resolve(inputDir, imageFile);
const absoluteImagePath = path.resolve(imagePath);
const existingScene = scenes.scenes.find(s => s.baseImagePath === absoluteImagePath);
if (existingScene) {
logger.info(`Skipping already processed image: ${imageFile}`);
continue;
}
logger.info(`Processing image: ${imageFile} `);
const prompt = `
Analyze the provided image and generate a JSON object with the following structure:
{
"scenes": [
{
"scene": "A descriptive title for the scene in the image.",
"imagePrompt": {
"description": "A detailed description of the image content.",
"style": "Art style or photography style of the image.",
"lighting": "Description of the lighting in the image.",
"outfit": "Description of the outfit or clothing style in the image.",
"location": "Description of the location or setting of the image.",
"poses": "Description of the poses or actions of any subjects in the image.",
"angle": "Description of the camera angle or perspective of the image.",
}
"videoPromp": "Based on the image, create a prompt for a video that shows what might happen next or brings the scene to life.",
"baseImagePath": "The absolute path of the base image."
}
]
}
Instructions: ${promptInstructions}
`;
try {
const result = await callLMStudioAPIWithFile(imagePath, prompt);
if (result && result.scenes) {
const newScene = result.scenes[0];
newScene.baseImagePath = absoluteImagePath; // Ensure the path is correct
scenes.scenes.push(newScene);
fs.writeFileSync(outputFilePath, JSON.stringify(scenes, null, 2));
logger.info(`Successfully processed and saved scene for: ${imageFile} `);
} else {
logger.error('Failed to get valid scene data from API for image:', imageFile);
}
} catch (error) {
logger.error(`Error processing image ${imageFile}: `, error);
}
}
}
processImages().catch(error => {
logger.error('An unexpected error occurred:', error);
});

View File

@ -0,0 +1,63 @@
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { generateVideo } from '../../lib/video-generator';
import dotenv from 'dotenv';
dotenv.config();
interface Scene {
scene: string;
videoPrompt: string;
baseImagePath: string;
}
const scenesFilePath = path.join(__dirname, 'scenes.json');
const generatedFolderPath = path.join(__dirname, '..', '..', '..', 'generated');
async function processScenes() {
try {
const scenesData = fs.readFileSync(scenesFilePath, 'utf-8');
const scenes: Scene[] = JSON.parse(scenesData).scenes;
for (const scene of scenes) {
try {
const hash = crypto.createHash('sha256').update(scene.baseImagePath).digest('hex');
const imageFileName = `${hash}.png`;
const imagePath = path.join(generatedFolderPath, imageFileName);
if (fs.existsSync(imagePath)) {
const outputVideoFileName = `${hash}.mp4`;
const outputVideoPath = path.join(generatedFolderPath, outputVideoFileName);
if (fs.existsSync(outputVideoPath)) {
console.log(`Video already exists for scene ${scene.scene}, skipping.`);
continue;
}
console.log(`Generating video for scene ${scene.scene}...`);
await generateVideo(
scene.videoPrompt,
imagePath,
outputVideoPath,
process.env.COMFY_BASE_URL!,
process.env.COMFY_OUTPUT_DIR!,
{ width: 1280, height: 720 }
);
console.log(`Video for scene ${scene.scene} saved to ${outputVideoPath}`);
} else {
console.warn(`Image not found for scene ${scene.scene}: ${imagePath}`);
}
} catch (e) {
continue;
}
}
} catch (error) {
console.error('Error processing scenes:', error);
}
}
processScenes();

View File

@ -0,0 +1,68 @@
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { generateVideo } from '../../lib/video-generator';
import { callLMStudioAPIWithFile } from '../../lib/lmstudio';
import dotenv from 'dotenv';
dotenv.config();
const inputFolderPath = path.join(__dirname, '..', '..', '..', 'input/static');
const generatedFolderPath = path.join(__dirname, '..', '..', '..', 'generated');
async function processImages() {
try {
const imageFiles = fs.readdirSync(inputFolderPath);
for (const imageFile of imageFiles) {
const imagePath = path.join(inputFolderPath, imageFile);
try {
const hash = crypto.createHash('sha256').update(imageFile).digest('hex');
const outputVideoFileName = `${hash}.mp4`;
const outputVideoPath = path.join(generatedFolderPath, outputVideoFileName);
if (fs.existsSync(outputVideoPath)) {
console.log(`Video already exists for image ${imageFile}, skipping.`);
continue;
}
console.log(`Generating video prompt for image ${imageFile}...`);
const promptResult = await callLMStudioAPIWithFile(
imagePath,
`
Generate a short, dancing video prompt for an image located at ${imagePath}.
Return the result in this format: {"result":""}
Instruction:
- Find best dancing expression based on the photo
`);
const videoPrompt = promptResult.result;
console.log(`Video prompt ${videoPrompt}`);
if (!videoPrompt) {
console.error(`Could not generate video prompt for image ${imageFile}`);
continue;
}
console.log(`Generating video for image ${imageFile}...`);
await generateVideo(
videoPrompt,
imagePath,
outputVideoPath,
process.env.COMFY_BASE_URL!,
process.env.COMFY_OUTPUT_DIR!,
{ width: 1280, height: 720 }
);
console.log(`Video for image ${imageFile} saved to ${outputVideoPath}`);
} catch (e) {
console.error(`Error processing image ${imageFile}:`, e);
continue;
}
}
} catch (error) {
console.error('Error processing images:', error);
}
}
processImages();

View File

@ -0,0 +1,227 @@
import { callLmstudio } from '../../lib/lmstudio';
import { logger } from '../../lib/logger';
import * as fs from 'fs/promises';
import dotenv from 'dotenv';
import path from 'path';
import puppeteer from 'puppeteer';
dotenv.config();
const SCROLL_SEARCH = 3; // scroll times on search results
const SCROLL_PIN = 3; // scroll times on pin page
const PINS_TO_COLLECT = 5;
// Hard-coded user prompt
const HARDCODED_USER_PROMPT = process.env.HARDCODED_USER_PROMPT || `
Generate 20 keywords for perfume brand photo. List of 20 most famous perfume brands, and its popular perfume names:
Example output : ["chanel N5", "dior j'adore", "gucci bloom"....]
`;
async function getPinUrlsFromPinterest(keyword: string, scrollCount = SCROLL_SEARCH, limit = PINS_TO_COLLECT): Promise<string[]> {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
const searchUrl = `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(keyword)}`;
await page.goto(searchUrl, { waitUntil: 'networkidle2' });
let pinLinks = new Set<string>();
for (let i = 0; i < scrollCount; i++) {
const linksBefore = pinLinks.size;
const newLinks = await page.$$eval('a', (anchors) =>
anchors.map((a) => a.href).filter((href) => href.includes('/pin/'))
);
newLinks.forEach(link => pinLinks.add(link));
if (pinLinks.size >= limit) {
break;
}
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise(r => setTimeout(r, 500 + Math.random() * 1000));
if (pinLinks.size === linksBefore) {
// If no new pins are loaded, stop scrolling
logger.info(`No new pins loaded for "${keyword}", stopping scroll.`);
break;
}
}
return Array.from(pinLinks).slice(0, limit);
} catch (error) {
logger.error(`Error while getting pin URLs from Pinterest for keyword "${keyword}":`, error);
return [];
} finally {
await browser.close();
}
}
async function downloadImagesFromPin(pinUrl: string, scrollTimes = SCROLL_PIN): Promise<string[]> {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
await page.goto(pinUrl, { waitUntil: 'networkidle2', timeout: 30000 });
for (let i = 0; i < scrollTimes; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise((r) => setTimeout(r, 700 + Math.random() * 800));
}
const imgs: string[] = await page.$$eval('img', imgs => {
const urls: string[] = imgs.map(img => {
const srcset = (img as HTMLImageElement).getAttribute('srcset') || '';
if (!srcset) {
return ''; // Ignore images without srcset
}
const parts = srcset.split(',').map(p => p.trim());
for (const part of parts) {
const match = part.match(/^(\S+)\s+4x$/);
if (match && match[1]) {
return match[1]; // Found the 4x version, return it
}
}
return ''; // No 4x version found for this image
}).filter(s => !!s && s.includes('pinimg')); // Filter out empty strings and non-pinterest images
return [...new Set(urls)]; // Return unique URLs
});
if (!imgs || imgs.length === 0) {
logger.warn(`No high-res images found on pin ${pinUrl}`);
return [];
}
const outDir = path.join(process.cwd(), 'download');
await fs.mkdir(outDir, { recursive: true });
const results: string[] = [];
for (let i = 0; i < imgs.length; i++) {
const src = imgs[i];
try {
const imgPage = await browser.newPage();
const resp = await imgPage.goto(src, { timeout: 30000, waitUntil: 'load' });
if (!resp) { await imgPage.close(); continue; }
const buffer = await resp.buffer();
const pinId = pinUrl.split('/').filter(Boolean).pop() || `pin_${Date.now()}`;
const timestamp = Date.now();
const outPath = path.join(outDir, `${pinId}_${timestamp}_${i}.png`);
await fs.writeFile(outPath, buffer);
results.push(outPath);
await imgPage.close();
} catch (err) {
logger.error(`Failed to download image ${src} from ${pinUrl}:`, err);
}
}
return results;
} catch (err) {
logger.error(`Failed to download images from ${pinUrl}:`, err);
return [];
} finally {
await browser.close();
}
}
// Re-usable helper to extract JSON embedded in text
function extractJsonFromText(text: string): any | null {
if (!text || typeof text !== 'string') return null;
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (fenced && fenced[1]) {
try { return JSON.parse(fenced[1].trim()); } catch (e) { /* fall through */ }
}
const brace = text.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
if (brace && brace[0]) {
try { return JSON.parse(brace[0]); } catch (e) { return null; }
}
// Attempt line-separated keywords fallback
const lines = text.split(/\r?\n/).map((l: string) => l.trim()).filter(Boolean);
if (lines.length > 1) return lines;
return null;
}
async function extractKeywordsFromPromptWithLmstudio(prompt: string, count = 5): Promise<string[]> {
const instruction = `You are given a short instruction describing the type of content to search for.
Return exactly a JSON array of ${count} short keyword phrases suitable for searching Pinterest. `;
try {
const res = await callLmstudio(`${instruction}\n\nInstruction: ${prompt}`);
if (!res) {
logger.warn('callLmstudio returned empty response for keyword extraction.');
return [];
}
let parsed: any;
if (typeof res === 'object' && res.text) {
parsed = extractJsonFromText(res.text);
} else if (typeof res === 'string') {
parsed = extractJsonFromText(res);
} else if (typeof res === 'object') {
parsed = res;
}
if (Array.isArray(parsed)) {
return parsed.map(String).slice(0, count);
}
if (typeof parsed === 'object' && parsed !== null) {
const maybe = parsed.keywords || parsed.list || parsed.items || parsed.keywords_list;
if (Array.isArray(maybe)) return maybe.map(String).slice(0, count);
}
const text = typeof res === 'string' ? res : (res && res.text) || JSON.stringify(res);
const lines = text.split(/\r?\n/).map((l: string) => l.replace(/^\d+[\).\s-]*/, '').trim()).filter(Boolean);
if (lines.length >= 1) {
return lines.slice(0, count);
}
logger.warn(`Could not parse keywords from LM Studio response: ${JSON.stringify(res)}`);
return [];
} catch (error) {
logger.error('Error during keyword extraction with callLmstudio:', error);
return [];
}
}
(async () => {
logger.info(`Starting photo download process with prompt: "${HARDCODED_USER_PROMPT}"`);
// 1. Extract keywords from the hardcoded prompt
const keywords = await extractKeywordsFromPromptWithLmstudio(HARDCODED_USER_PROMPT, 20); // Using 5 keywords to get a good variety
if (!keywords || keywords.length === 0) {
logger.error("Could not extract keywords from prompt. Exiting.");
return;
}
logger.info(`Extracted keywords: ${keywords.join(', ')}`);
// 2. Search Pinterest for each keyword and collect pin URLs
let allPinUrls = new Set<string>();
for (const keyword of keywords) {
logger.info(`Searching Pinterest for keyword: "${keyword}"`);
const pinUrls = await getPinUrlsFromPinterest(keyword, SCROLL_SEARCH, PINS_TO_COLLECT);
pinUrls.forEach(url => allPinUrls.add(url));
}
const finalPinUrls = Array.from(allPinUrls);
logger.info(`Collected ${finalPinUrls.length} unique pin URLs to process.`);
// 3. Go through each pin URL, scroll, and download all photos
let totalDownloads = 0;
for (const pinUrl of finalPinUrls) {
try {
logger.info(`Processing pin: ${pinUrl}`);
const downloadedPaths = await downloadImagesFromPin(pinUrl, SCROLL_PIN);
if (downloadedPaths.length > 0) {
logger.info(`Successfully downloaded ${downloadedPaths.length} images from ${pinUrl}`);
totalDownloads += downloadedPaths.length;
} else {
logger.warn(`No images were downloaded from ${pinUrl}`);
}
} catch (error) {
logger.error(`An error occurred while processing pin ${pinUrl}:`, error);
}
}
logger.info(`Photo download process finished. Total images downloaded: ${totalDownloads}`);
})();

14021
src/pinterest_keywords.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,669 @@
import { downloadImagesFromPinterestPin } from './lib/downloader';
import { callOpenAIWithFile } from './lib/openai';
import { generateVideo } from './lib/video-generator';
import { generateImage as generateImageMixStyle } from './lib/image-generator-mix-style';
import { generateImage as generateImage } from './lib/image-generator';
import { logger } from './lib/logger';
import * as fs from 'fs/promises';
import dotenv from 'dotenv';
import path from 'path';
import puppeteer from 'puppeteer';
import { VideoModel } from './lib/db/video';
dotenv.config();
const RUN_ONCE = (process.env.RUN_ONCE || 'false').toLowerCase() === 'true';
const USE_REFERENCE_IMAGE = (process.env.USE_REFERENCE_IMAGE || 'true').toLowerCase() === 'true';
// Utility: extract JSON substring from a text.
// Tries fenced ```json``` blocks first, otherwise extracts first {...} span.
function extractJsonFromText(text: string): any | null {
if (!text || typeof text !== 'string') return null;
// Try fenced code block with optional json language
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (fenced && fenced[1]) {
try {
return JSON.parse(fenced[1].trim());
} catch (e) {
// fall through to brace extraction
}
}
// Try to extract first {...} match
const brace = text.match(/\{[\s\S]*\}/);
if (brace && brace[0]) {
try {
return JSON.parse(brace[0]);
} catch (e) {
return null;
}
}
return null;
}
// Wrapper to call OpenAI with an image and prompt, and extract JSON reliably.
// - Uses callOpenAIWithFile to pass the image.
// - Tries to parse JSON from response if needed.
// - Retries up to maxRetries times (default 5) when parsing fails or an error occurs.
async function callOpenAIWithFileAndExtract(imagePath: string, prompt: string, maxRetries = 5): Promise<any | null> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await callOpenAIWithFile(imagePath, prompt);
// callOpenAIWithFile may return an object or parsed JSON already
if (res && typeof res === 'object') {
return res;
}
if (typeof res === 'string') {
const parsed = extractJsonFromText(res);
if (parsed) return parsed;
}
// unexpected shape -> retry
logger.warn(`callOpenAIWithFileAndExtract: attempt ${attempt} returned unexpected result. Retrying...`);
} catch (err) {
logger.warn(`callOpenAIWithFileAndExtract: attempt ${attempt} failed: ${err}`);
}
}
logger.error(`callOpenAIWithFileAndExtract: failed to get valid JSON after ${maxRetries} attempts`);
return null;
}
const servers = [
{
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},
{
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
},
].filter((s): s is { baseUrl: string; outputDir: string } => !!s.baseUrl && !!s.outputDir);
interface GenerationTask {
pinUrl: string;
imagePrompt: string;
videoPrompt: string;
imageFileName: string;
renamedImagePaths: string[];
generatedImagePath?: string;
genre: string;
subGenre: string;
scene: string;
action: string;
camera: string;
videoInstructions?: string[];
}
async function getPromptsForImage(imagePaths: string[], pinUrl: string, genre: string, subGenre: string, videoInstructions: string[] = []): Promise<GenerationTask | null> {
const pinId = pinUrl.split('/').filter(Boolean).pop() || `pin_${Date.now()}`;
const timestamp = new Date().getTime();
const imageFileName = `${pinId}_${timestamp}.png`;
const renamedImagePaths: string[] = [];
for (let i = 0; i < imagePaths.length; i++) {
const renamedPath = path.join(path.dirname(imagePaths[i]), `${pinId}_${timestamp}_${i}.png`);
await fs.rename(imagePaths[i], renamedPath);
renamedImagePaths.push(renamedPath);
}
logger.debug(`Renamed source images to: ${renamedImagePaths.join(', ')}`);
const imageForPrompt = renamedImagePaths[Math.floor(Math.random() * renamedImagePaths.length)];
try {
// Step 1: Detect main object
const step1Prompt = `
Return exactly one JSON object and nothing else: { "mainobject": "..." }.
Look at the provided image and determine the single most prominent/main object or subject in the scene.
Answer with a short noun or short phrase (no extra commentary).
If unsure, give the best concise guess.
`;
const step1Res = await callOpenAIWithFileAndExtract(imageForPrompt, step1Prompt, 5);
const mainobject = (step1Res && (step1Res.mainobject || step1Res.mainObject || step1Res.object)) ? String(step1Res.mainobject || step1Res.mainObject || step1Res.object).trim() : '';
if (!mainobject) {
throw new Error('Could not detect main object');
}
logger.info(`Detected main object for ${imageForPrompt}: ${mainobject}`);
// Step 2: Determine best action for this scene
const step2Prompt = `
You have access to the image and the detected main object: "${mainobject}".
Decide which single action type best fits this scene from the list:
- no action
- micro animation (animate object but small movement)
- big movement
- impossible movement
- Dance ( if its woman portrait )
Return exactly one JSON object and nothing else: { "actiontype": "..." }.
Do not add commentary. Choose the single best option from the list above.
`;
const step2Res = await callOpenAIWithFileAndExtract(imageForPrompt, step2Prompt, 5);
const actiontype = (step2Res && (step2Res.actiontype || step2Res.actionType)) ? String(step2Res.actiontype || step2Res.actionType).trim() : '';
if (!actiontype) {
throw new Error('Could not determine action type');
}
logger.info(`Decided action type for ${imageForPrompt}: ${actiontype}`);
// Step 3: Ask OpenAI what is the best camera work for the scene
const step3Prompt = `
Given the image and the following information:
- main object: "${mainobject}"
- chosen action type: "${actiontype}"
From the options below pick the single best camera approach for this scene:
- static camera
- pan
- rotation
- follow the moving object
- zoom to the object
- impossible camera work
Return exactly one JSON object and nothing else: { "cameraworkType": "..." }.
Choose one of the listed options and do not add commentary.
`;
const step3Res = await callOpenAIWithFileAndExtract(imageForPrompt, step3Prompt, 5);
const cameraworkType = (step3Res && (step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera)) ? String(step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera).trim() : '';
if (!cameraworkType) {
throw new Error('Could not determine camera work');
}
logger.info(`Decided camera work for ${imageForPrompt}: ${cameraworkType}`);
let videoInstruction = "";
if (videoInstructions && videoInstructions.length > 0) {
const videoInstructionPrompt = `
Given the image and the following information:
- main object: "${mainobject}"
From the options below pick the single best camera approach for this scene:
${videoInstructions.join(",\r\n")}
Return exactly one JSON object and nothing else: { "videoInstruction": "..." }.
Choose one of the listed options and do not add commentary.
`;
const videoInstructionRes = await callOpenAIWithFileAndExtract(imageForPrompt, videoInstructionPrompt, 5);
const videoInstructionFinalRes = (step3Res && (videoInstructionRes.videoInstruction || videoInstructionRes.videoInstruction || videoInstructionRes.camera)) ? String(videoInstructionRes.videoInstruction || videoInstructionRes.videoInstruction || videoInstructionRes.camera).trim() : '';
if (videoInstructionFinalRes)
videoInstruction = videoInstructionFinalRes
}
// Step 4: Generate final video prompt (and image prompt) using all gathered info
const finalPrompt = `
Return exactly one JSON object: { "scene": "...", "action":"...", "camera":"...", "image_prompt":"...", "videoPrompt":"..." } and nothing else.
Write "videoPrompt" in 100150 words, present tense, plain concrete language.
Write "image_prompt" as a concise, detailed prompt suitable for generating a similar image.
HARD RULES (must comply for videoPrompt):
- One continuous shot. Real-time 8 seconds. No edits.
- Fixed location and vantage. Do not change background or angle.
- Lens and focal length locked. No zooms, no close-ups that imply a lens change.
- Camera motion: at most subtle pan/tilt/dolly within 1 meter while staying in the same spot.
- Keep framing consistent. No “another shot/meanwhile.”
- Use clear simple sentences. No metaphors or poetic language.
Here is information of the scene, please generate fields accordingly:
Detected Main Object: ${mainobject}
Suggested Action Type: ${actiontype}
Suggested Camera Work: ${cameraworkType}
Genre: ${genre}
Sub-Genre: ${subGenre}
${videoInstruction ? 'video instruction:' + videoInstruction : ""}
`;
const finalRes = await callOpenAIWithFileAndExtract(imageForPrompt, finalPrompt, 5);
const scene = finalRes && (finalRes.scene || finalRes.Scene) ? String(finalRes.scene) : '';
const action = finalRes && (finalRes.action || finalRes.Action) ? String(finalRes.action) : '';
const camera = finalRes && (finalRes.camera || finalRes.Camera) ? String(finalRes.camera) : '';
const imagePrompt = finalRes && (finalRes.image_prompt || finalRes.imagePrompt || finalRes.image_prompt) ? String(finalRes.image_prompt || finalRes.imagePrompt) : '';
const videoPrompt = finalRes && (finalRes.videoPrompt || finalRes.video_prompt || finalRes.video_prompt) ? String(finalRes.videoPrompt || finalRes.video_prompt) : '';
if (!imagePrompt || !videoPrompt) {
throw new Error('Final LM output did not include image_prompt or videoPrompt');
}
logger.info(`Image prompt for ${imageForPrompt}:`, imagePrompt);
logger.info(`Video prompt for ${imageForPrompt}:`, videoPrompt);
return { pinUrl, imagePrompt, videoPrompt, imageFileName, renamedImagePaths, genre, subGenre, scene, action, camera };
} catch (error) {
logger.error(`Failed to get prompts for ${imageForPrompt}:`, error);
for (const p of renamedImagePaths) {
try {
await fs.unlink(p);
} catch (cleanupError) {
// ignore
}
}
return null;
}
}
async function generateImageForTask(task: GenerationTask, server: { baseUrl: string; outputDir: string; }): Promise<string | null> {
const { imagePrompt, imageFileName, renamedImagePaths } = task;
const { baseUrl, outputDir } = server;
const inputDir = outputDir.replace("output", "input");
const sourceFileNames: string[] = [];
try {
if (USE_REFERENCE_IMAGE) {
// Copy renamed source images to the server input directory
for (const sourcePath of renamedImagePaths) {
const fileName = path.basename(sourcePath);
const destPath = path.join(inputDir, fileName);
await fs.copyFile(sourcePath, destPath);
sourceFileNames.push(fileName);
logger.info(`Copied ${sourcePath} to ${destPath}`);
}
// generateImageMixStyle expects two source files; if we only have one, pass the same one twice
const srcA = sourceFileNames[0];
const srcB = sourceFileNames[1] || sourceFileNames[0];
const generatedImagePath = await generateImageMixStyle(
imagePrompt,
srcA,
srcB,
imageFileName,
baseUrl,
outputDir,
{ width: 1280, height: 720 }
);
return generatedImagePath;
} else {
// Use Pinterest images only to create the prompt; generate final image using the single-image generator
const generatedImagePath = await generateImage(
imagePrompt,
imageFileName,
baseUrl,
outputDir,
'qwen',
{ width: 1280, height: 720 }
);
return generatedImagePath;
}
} catch (error) {
logger.error(`Failed to generate image for ${imageFileName} on server ${baseUrl}:`, error);
return null;
} finally {
// cleanup local renamed images and any files copied to the server input dir
for (const sourcePath of renamedImagePaths) {
try {
await fs.unlink(sourcePath);
logger.debug(`Deleted source image: ${sourcePath}`);
} catch (error) {
logger.error(`Failed to delete source image ${sourcePath}:`, error);
}
}
for (const fileName of sourceFileNames) {
try {
const serverPath = path.join(inputDir, fileName);
await fs.unlink(serverPath);
logger.debug(`Deleted server image: ${serverPath}`);
} catch (error) {
logger.error(`Failed to delete server image ${fileName}:`, error);
}
}
}
}
async function getPinUrlFromPinterest(keyword: string): Promise<string | null> {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
const searchUrl = `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(keyword)}`;
await page.goto(searchUrl, { waitUntil: 'networkidle2' });
const scrollCount = Math.floor(Math.random() * 5) + 1;
logger.info(`Scrolling ${scrollCount} times...`);
for (let i = 0; i < scrollCount; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 1000));
}
const pinLinks = await page.$$eval('a', (anchors) =>
anchors.map((a) => a.href).filter((href) => href.includes('/pin/'))
);
if (pinLinks.length > 0) {
return pinLinks[Math.floor(Math.random() * pinLinks.length)];
}
return null;
} catch (error) {
logger.error('Error while getting pin URL from Pinterest:', error);
return null;
} finally {
await browser.close();
}
}
(async () => {
// Load pinterest keywords JSON, pick up to 20 subGenres and choose 1 pinId per subGenre
const keywordsFilePath = path.resolve(process.cwd(), 'src', 'pinterest_keywords.json');
let allKeywords: { genre: string; subGenre: string; pinIds?: string[]; pinId?: string[], videoInstructions?: string[] }[] = [];
try {
const raw = await fs.readFile(keywordsFilePath, 'utf-8');
allKeywords = JSON.parse(raw);
} catch (err) {
logger.error('Failed to read pinterest keywords JSON:', err);
return;
}
/*
allKeywords = allKeywords.filter(a => {
return (a.genre == "dance" && a.subGenre == "women tiktok") ||
(a.genre == "dance" && a.subGenre == "street real") ||
(a.genre == "dance" && a.subGenre == "man street") ||
(a.genre == "dance" && a.subGenre == "group street") ||
(a.genre == "dance" && a.subGenre == "group street") ||
});
*/
allKeywords = allKeywords.filter(a => {
return (a.genre == "dance") || (a.genre == "epic")
});
function shuffle<T>(arr: T[]): T[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
//const selectedEntries = shuffle(allKeywords.slice()).slice(0, Math.min(20, allKeywords.length));
const selectedEntries = shuffle(allKeywords);
// Download up to `count` images from a pin URL by opening the pin page and scro lling up to 5 times to trigger lazy loading
// Returns an array of saved image paths (may be empty)
async function downloadOneImageFromPin(pinUrl: string, count: number = 1): Promise<string[]> {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
await page.goto(pinUrl, { waitUntil: 'networkidle2', timeout: 30000 });
for (let i = 0; i < 3; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise((r) => setTimeout(r, 700 + Math.random() * 800));
}
const imgs: string[] = await page.$$eval('img', imgs => {
// For each <img> try to extract the 4x (original) URL from srcset.
// srcset example:
// "https://i.pinimg.com/236x/...jpg 1x, https://i.pinimg.com/474x/...jpg 2x, https://i.pinimg.com/736x/...jpg 3x, https://i.pinimg.com/originals/...jpg 4x"
const urls: string[] = imgs.map(img => {
const srcset = (img as HTMLImageElement).getAttribute('srcset') || '';
if (!srcset) return '';
const parts = srcset.split(',').map(p => p.trim());
for (const part of parts) {
const m = part.match(/^(\S+)\s+4x$/);
if (m && m[1]) return m[1];
}
// fallback: if src contains "originals" return src
const src = (img as HTMLImageElement).src || '';
if (src.includes('/originals/')) return src;
return '';
}).filter(s => !!s && s.includes('pinimg'));
return urls;
});
if (!imgs || imgs.length === 0) {
logger.warn(`No image src (4x) found on pin page ${pinUrl}`);
return [];
}
// shuffle and pick up to `count` unique images
const shuffled = imgs.slice().sort(() => 0.5 - Math.random());
const chosen = shuffled.slice(0, Math.min(count, shuffled.length));
const outDir = path.join(process.cwd(), 'download');
await fs.mkdir(outDir, { recursive: true });
const results: string[] = [];
for (let i = 0; i < chosen.length; i++) {
const src = chosen[i];
try {
const imgPage = await browser.newPage();
const resp = await imgPage.goto(src, { timeout: 30000, waitUntil: 'networkidle2' });
if (!resp) {
logger.warn(`Failed to fetch image ${src} from ${pinUrl}`);
await imgPage.close();
continue;
}
const buffer = await resp.buffer();
const pinId = pinUrl.split('/').filter(Boolean).pop() || `pin_${Date.now()}`;
const timestamp = Date.now();
const outPath = path.join(outDir, `${pinId}_${timestamp}_${i}.png`);
await fs.writeFile(outPath, buffer);
results.push(outPath);
await imgPage.close();
} catch (err) {
logger.error(`Failed to download image ${src} from ${pinUrl}:`, err);
}
}
return results;
} catch (err) {
logger.error(`Failed to download images from ${pinUrl}:`, err);
return [];
} finally {
await browser.close();
}
}
const numberOfPinIds = Number(process.env.NUMBER_OF_PINIDS) || 20;
// Build keywords list with single chosen pinId per selected subGenre
const keywords: {
genre: string; subGenre: string; pinIds: string[], videoInstructions?: string[]
}[] = [];
for (const entry of selectedEntries) {
const pinIds = (entry.pinIds || entry.pinId) as string[] | undefined;
if (!Array.isArray(pinIds) || pinIds.length === 0) continue;
const chosenPinId = pinIds.splice(0, numberOfPinIds);
keywords.push({ genre: entry.genre, subGenre: entry.subGenre, pinIds: chosenPinId, videoInstructions: entry.videoInstructions });
}
if (keywords.length === 0) {
logger.error("No keywords/pinIds available from pinterest_keywords.json. Exiting.");
return;
}
if (servers.length === 0) {
logger.error("No servers configured. Please check your .env file.");
return;
}
type pinIdsType = {
pinId: string,
genreSubGenre: { genre: string, subGenre: string, pinIds: string[], videoInstructions: string[] }
};
while (true) {
const generationTasks: GenerationTask[] = [];
const allPinIds: pinIdsType[] = keywords.reduce<pinIdsType[]>((acc, curr) => {
const videoInstructions = curr.videoInstructions ?? [];
for (const id of curr.pinIds ?? []) {
acc.push({
pinId: id,
genreSubGenre: {
genre: curr.genre,
subGenre: curr.subGenre,
pinIds: curr.pinIds,
videoInstructions,
},
});
}
return acc;
}, []);
const pickedUpPinIds: pinIdsType[] = shuffle(allPinIds).slice(0, 30);
for (const row of pickedUpPinIds) {
const { genreSubGenre, pinId } = row;
const genre = genreSubGenre.genre;
const subGenre = genreSubGenre.subGenre;
const pin = `https://www.pinterest.com/pin/${pinId}/`;
logger.info(`--- Starting processing for pin: ${pin} ---`);
// download images from the pin page (pass desired count as second arg)
const downloadedImagePaths = await downloadOneImageFromPin(pin, 20);
if (!downloadedImagePaths || downloadedImagePaths.length === 0) {
logger.warn(`No images were downloaded for pin ${pin}. Skipping.`);
continue;
}
const selectedImages = downloadedImagePaths.sort(() => 0.5 - Math.random()).slice(0, 2);
logger.info(`--- Downloaded ${selectedImages.length} image(s) for processing ---`);
// proceed if we have at least one image
if (selectedImages.length >= 1) {
const task = await getPromptsForImage(selectedImages, pin, genre, subGenre, genreSubGenre.videoInstructions);
if (task) {
task.videoInstructions = genreSubGenre.videoInstructions;
generationTasks.push(task);
}
} else {
logger.warn(`Skipping pin ${pin} as it did not yield images.`);
for (const imagePath of selectedImages) {
try {
await fs.unlink(imagePath);
} catch (error) {
logger.error(`Failed to delete image ${imagePath}:`, error);
}
}
}
}
// --- Image Generation Phase ---
logger.info(`--- Starting image generation for ${generationTasks.length} tasks ---`);
if (generationTasks.length > 0) {
// Distribute tasks evenly across servers (round-robin) and run each server's tasks concurrently.
const tasksByServer: GenerationTask[][] = servers.map(() => []);
generationTasks.forEach((t, idx) => {
const serverIndex = idx % servers.length;
tasksByServer[serverIndex].push(t);
});
await Promise.all(servers.map(async (server, serverIndex) => {
const tasks = tasksByServer[serverIndex];
if (!tasks || tasks.length === 0) return;
logger.info(`Server ${server.baseUrl} starting ${tasks.length} image task(s)`);
const results = await Promise.all(tasks.map(t => generateImageForTask(t, server)));
for (let i = 0; i < tasks.length; i++) {
const res = results[i];
if (res) tasks[i].generatedImagePath = res;
}
logger.info(`Server ${server.baseUrl} finished image task(s)`);
}));
}
logger.info("--- Finished image generation ---");
// --- Video Generation Phase ---
logger.info(`--- Starting video generation for ${generationTasks.length} tasks ---`);
// Prepare tasks that have generatedImagePath
const tasksWithImages = generationTasks.filter(t => t.generatedImagePath);
if (tasksWithImages.length > 0) {
// Distribute video tasks evenly across servers and run them concurrently per server.
const videoTasksByServer: GenerationTask[][] = servers.map(() => []);
tasksWithImages.forEach((t, idx) => {
const serverIndex = idx % servers.length;
videoTasksByServer[serverIndex].push(t);
});
await Promise.all(servers.map(async (server, serverIndex) => {
const tasks = videoTasksByServer[serverIndex];
if (!tasks || tasks.length === 0) return;
logger.info(`Server ${server.baseUrl} starting ${tasks.length} video task(s)`);
// Run all tasks for this server concurrently
await Promise.allSettled(tasks.map(async (task) => {
const inputDir = server.outputDir.replace("output", "input");
if (!task.generatedImagePath) {
logger.warn(`Skipping task ${task.imageFileName} on server ${server.baseUrl} - missing generatedImagePath`);
return;
}
const generatedImageName = path.basename(task.generatedImagePath);
const serverImagePath = path.join(inputDir, generatedImageName);
try {
// copy image to server input dir
await fs.copyFile(task.generatedImagePath, serverImagePath);
logger.info(`Copied ${task.generatedImagePath} to ${serverImagePath}`);
const videoFileName = task.imageFileName.replace('.png', '.mp4');
const videoPath = await generateVideo(
task.videoPrompt,
generatedImageName,
videoFileName,
server.baseUrl,
server.outputDir,
{ width: 1280, height: 720 }
);
if (videoPath) {
const videoData = {
genre: task.genre,
sub_genre: task.subGenre,
scene: task.scene,
action: task.action,
camera: task.camera,
image_prompt: task.imagePrompt,
video_prompt: task.videoPrompt,
image_path: task.generatedImagePath!,
video_path: videoPath,
};
const videoId = await VideoModel.create(videoData);
logger.info(`Successfully saved video record to database with ID: ${videoId}`);
const newImageName = `${videoId}_${task.genre}_${task.subGenre}${path.extname(task.generatedImagePath)}`;
const newVideoName = `${videoId}_${task.genre}_${task.subGenre}${path.extname(videoPath)}`;
const newImagePath = path.join(path.dirname(task.generatedImagePath), newImageName);
const newVideoPath = path.join(path.dirname(videoPath), newVideoName);
await fs.rename(task.generatedImagePath, newImagePath);
await fs.rename(videoPath, newVideoPath);
await VideoModel.update(videoId, {
image_path: newImagePath,
video_path: newVideoPath,
});
logger.info(`Renamed files and updated database record for video ID: ${videoId}`);
} else {
logger.warn(`Video generation returned no path for task ${task.imageFileName} on server ${server.baseUrl}`);
}
} catch (error) {
logger.error('An error occurred during video generation or database operations:', error);
} finally {
try {
await fs.unlink(serverImagePath);
logger.debug(`Deleted server image: ${serverImagePath}`);
} catch (error) {
logger.error(`Failed to delete server image ${serverImagePath}:`, error);
}
}
}));
logger.info(`Server ${server.baseUrl} finished video task(s)`);
}));
} else {
logger.info('No generated images available for video generation.');
}
logger.info("--- Finished video generation ---");
if (RUN_ONCE) {
logger.info('RUN_ONCE=true - exiting after a single iteration of generation.');
return;
}
}
})();

View File

@ -0,0 +1,75 @@
import { convertImage } from '../lib/image-converter';
import * as fs from 'fs-extra';
import * as path from 'path';
import dotenv from 'dotenv';
dotenv.config();
const inputDir = path.join(__dirname, '../../input');
const outputDir = path.join(__dirname, '../../generated/clearned');
const comfyUrl = process.env.SERVER1_COMFY_BASE_URL;
const comfyOutputDir = process.env.SERVER1_COMFY_OUTPUT_DIR;
if (!comfyUrl || !comfyOutputDir) {
console.error("ComfyUI URL or Output Directory is not set in environment variables.");
process.exit(1);
}
const comfyInputDir = comfyOutputDir.replace("output", "input");
async function processImages() {
await fs.ensureDir(outputDir);
const files = await fs.readdir(inputDir);
let index = 1;
for (const file of files) {
const sourceFilePath = path.join(inputDir, file);
const stats = await fs.stat(sourceFilePath);
if (stats.isFile()) {
console.log(`Processing ${file}...`);
const comfyInputPath = path.join(comfyInputDir, file);
try {
// 1. Copy file to ComfyUI input directory
await fs.copy(sourceFilePath, comfyInputPath);
console.log(`Copied ${file} to ComfyUI input.`);
const prompt = "请从图1中提取主要主体把背景设置为浅灰色并让主体正面朝向制作成产品照片。";
// 2. Call convertImage with correct parameters
const generatedFilePath = await convertImage(prompt, file, comfyUrl!, comfyOutputDir!);
if (generatedFilePath && await fs.pathExists(generatedFilePath)) {
const outputFilename = `clearned_${index}.png`;
const finalOutputPath = path.join(outputDir, outputFilename);
// 3. Move the generated file to the final destination
await fs.move(generatedFilePath, finalOutputPath, { overwrite: true });
console.log(`Saved cleaned image to ${finalOutputPath}`);
index++;
// 4. Delete the original file from the script's input directory
await fs.unlink(sourceFilePath);
console.log(`Deleted original file: ${file}`);
}
// 5. Clean up the file from ComfyUI input directory
await fs.unlink(comfyInputPath);
console.log(`Cleaned up ${file} from ComfyUI input.`);
} catch (error) {
console.error(`Failed to process ${file}:`, error);
// If something fails, make sure to clean up the copied file if it exists
if (await fs.pathExists(comfyInputPath)) {
await fs.unlink(comfyInputPath);
}
}
}
}
}
processImages().catch(console.error);

View File

@ -0,0 +1,88 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import dotenv from 'dotenv';
import { readJsonToPng, embedJsonToPng } from '../lib/util';
import { convertImage } from '../lib/image-converter';
dotenv.config();
const inputDir = './generated/prompts';
const outputDir = './generated/image';
const COMFY_BASE_URL = process.env.SERVER1_COMFY_BASE_URL!;
const COMFY_OUTPUT_DIR = process.env.SERVER1_COMFY_OUTPUT_DIR!;
interface PngMetadata {
prompts: {
imagePrompt: string;
videoPrompt: string;
}[];
}
async function main() {
await fs.mkdir(outputDir, { recursive: true });
const files = await fs.readdir(inputDir);
let generatedImageIndex = 0;
for (const file of files) {
if (path.extname(file).toLowerCase() !== '.png') {
continue;
}
const inputFile = path.join(inputDir, file);
const metadata = await readJsonToPng(inputFile) as PngMetadata;
if (metadata && metadata.prompts && Array.isArray(metadata.prompts)) {
console.log(`Processing ${file} with ${metadata.prompts.length} prompt pairs.`);
const inputfolderFullpath = COMFY_OUTPUT_DIR.replace("output", "input");
await fs.copyFile(inputFile, path.join(inputfolderFullpath, file));
for (const promptPair of metadata.prompts) {
const { imagePrompt, videoPrompt } = promptPair;
const newFileName = `cleaned_prompt_generated_${generatedImageIndex}.png`;
generatedImageIndex++;
const outputPath = path.join(outputDir, newFileName);
try {
await fs.access(outputPath);
console.log(`File ${newFileName} already exists, skipping.`);
continue;
} catch (error) {
// File does not exist, proceed with generation
}
console.log(`Generating image for prompt: "${imagePrompt}"`);
try {
const generatedFilePath = await convertImage(
imagePrompt,
file, // Using the same image for both inputs as per interpretation
COMFY_BASE_URL,
COMFY_OUTPUT_DIR
);
// The convertImage function saves the file in a generic location.
// We need to move it to the correct location with the correct name.
await fs.rename(generatedFilePath, outputPath);
const newMetadata = {
imagePrompt: imagePrompt,
videoPrompt: videoPrompt
};
await embedJsonToPng(outputPath, newMetadata);
console.log(`Successfully generated and saved ${newFileName} with metadata.`);
} catch (error) {
console.error(`Error generating image for prompt "${imagePrompt}":`, error);
}
}
} else {
console.log(`Skipping ${file}, no valid prompts metadata found.`);
}
}
}
main().catch(console.error);

View File

@ -0,0 +1,169 @@
import * as fs from 'fs';
import * as path from 'path';
import { callLMStudioAPIWithFile, callLmstudio } from '../lib/lmstudio';
import { embedJsonToPng } from '../lib/util';
import { downloadImagesFromPinterestSearch } from '../lib/pinterest';
import { logger } from '../lib/logger';
import sharp from 'sharp';
const INPUT_DIR = path.join(process.cwd(), 'input');
const OUTPUT_DIR = path.join(process.cwd(), 'generated', 'prompts');
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
async function generatePromptsForImage(imagePath: string, index: number) {
const outputFilePath = path.join(OUTPUT_DIR, `cleaned_prompt_${index}.png`);
logger.info(`Processing image: ${path.basename(imagePath)} -> ${path.basename(outputFilePath)}`);
try {
// Step 1: Detect main object and generate colors from the input image
const colorGenerationPrompt = `
You are a creative assistant. Analyze the provided image.
Identify the main subject product ( not a product name).
Then, list exactly five colors related to this subject:
- Two colors that are common for this object.
- Two colors that are uncommon but plausible.
- One color that is completely crazy or surreal for this object.
Output strictly in this JSON format:
{
"result": {
"main_object": "the identified noun",
"colors": [
"color1",
"color2",
"color3",
"color4",
"color5"
]
}
}
`;
const colorResponse = await callLMStudioAPIWithFile(imagePath, colorGenerationPrompt);
const { main_object, colors } = colorResponse.result;
if (!main_object || !Array.isArray(colors) || colors.length !== 5) {
logger.error(`Failed to get a valid main object and color list for ${imagePath}.`);
return;
}
logger.info(`Main object: "${main_object}", Colors: ${colors.join(', ')}`);
const prompts: { imagePrompt: string, videoPrompt: string }[] = [];
const themes = ["special", "unique", "beautiful", "crazy", "funny"];
// Step 2: Iterate through each color
for (const color of colors) {
const randomTheme = themes[Math.floor(Math.random() * themes.length)];
const pinterestQuery = `${main_object} product photo ${color} background ${randomTheme}`;
logger.info(`Searching Pinterest for: "${pinterestQuery}"`);
// Step 3: Get an image from Pinterest
const downloadedImages = await downloadImagesFromPinterestSearch(pinterestQuery, 1);
if (downloadedImages.length === 0) {
logger.warn(`Could not find an image on Pinterest for query: "${pinterestQuery}"`);
continue;
}
const pinterestImagePath = downloadedImages[0];
logger.info(`Downloaded Pinterest image: ${pinterestImagePath}`);
// Step 4: Generate a detailed prompt from the Pinterest image
const imagePromptRequest = `
You are an expert in generating descriptive prompts for image generation models.
Analyze the provided image and describe it in a single, detailed paragraph.
Focus on style, mood, lighting, color palette, sub-objects, and composition.
Do not mention the main object itself. The prompt should be about the scene.
Output strictly in this JSON format:
{
"result": "your generated prompt here"
}
`;
const imagePromptResponse = await callLMStudioAPIWithFile(pinterestImagePath, imagePromptRequest);
const imagePrompt = imagePromptResponse.result;
if (imagePrompt) {
logger.info(`Generated image prompt for color ${color}: "${imagePrompt}"`);
// Step 5: Generate a matching video prompt
const videoPromptRequest = `
You are a creative director for a short, stylish video ad.
Based on the provided image and the following scene description, generate an attractive video prompt.
Main Subject: ${main_object}
Scene Description: ${imagePrompt}
The video prompt should:
- Be in English and approximately 50 words.
- Describe one clear action involving the main subject.
- Include one specific camera movement (e.g., slow zoom in, orbiting shot, push-in, pull-out).
- Be dynamic and visually appealing.
Output strictly in this JSON format:
{
"result": "your generated video prompt here"
}
`;
const videoPromptResponse = await callLMStudioAPIWithFile(pinterestImagePath, videoPromptRequest);
const videoPrompt = videoPromptResponse.result;
if (videoPrompt) {
logger.info(`Generated video prompt for color ${color}: "${videoPrompt}"`);
prompts.push({ imagePrompt, videoPrompt });
} else {
logger.warn(`Failed to generate a video prompt for ${pinterestImagePath}`);
}
} else {
logger.warn(`Failed to generate an image prompt for ${pinterestImagePath}`);
}
}
if (prompts.length === 0) {
logger.error(`No prompt pairs were generated for ${imagePath}. Aborting.`);
return;
}
// Step 6: Embed all prompts into the original image and save to the new location
const metadata = {
prompts: prompts
};
// Convert original image to a valid PNG at the output path before embedding
await sharp(imagePath)
.toFormat('png')
.toFile(outputFilePath);
await embedJsonToPng(outputFilePath, metadata);
logger.info(`Successfully generated prompts and saved metadata to ${outputFilePath}`);
} catch (error) {
logger.error(`An error occurred while processing ${imagePath}:`, error);
}
}
async function main() {
try {
const files = fs.readdirSync(INPUT_DIR);
const imageFiles = files.filter(file => /\.(png|jpg|jpeg)$/i.test(file));
if (imageFiles.length === 0) {
console.log('No images found in the input directory.');
return;
}
for (let i = 0; i < imageFiles.length; i++) {
const imageFile = imageFiles[i];
const imagePath = path.join(INPUT_DIR, imageFile);
await generatePromptsForImage(imagePath, i);
}
console.log('All images processed.');
} catch (error) {
console.error('An error occurred in the main process:', error);
}
}
main();

View File

@ -0,0 +1,74 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import dotenv from 'dotenv';
import { readJsonToPng } from '../lib/util';
import { generateVideo } from '../lib/video-generator';
dotenv.config();
const inputDir = './input';
const outputDir = './generated/video';
const COMFY_BASE_URL = process.env.SERVER1_COMFY_BASE_URL!;
const COMFY_OUTPUT_DIR = process.env.SERVER1_COMFY_OUTPUT_DIR!;
interface PngMetadata {
imagePrompt: string;
videoPrompt: string;
}
async function main() {
await fs.mkdir(outputDir, { recursive: true });
const files = await fs.readdir(inputDir);
const pngFiles = files.filter(file => path.extname(file).toLowerCase() === '.png');
for (let i = 0; i < pngFiles.length; i++) {
const file = pngFiles[i];
const inputFile = path.join(inputDir, file);
const metadata = await readJsonToPng(inputFile) as PngMetadata;
if (metadata && metadata.videoPrompt) {
console.log(`Processing ${file} for video generation.`);
const originalFileName = path.parse(file).name;
const nameParts = originalFileName.split('_');
const promptIndex = nameParts[nameParts.length - 1];
const newFileName = `product_${i}_${promptIndex}.mp4`;
const outputPath = path.join(outputDir, newFileName);
try {
await fs.access(outputPath);
console.log(`File ${newFileName} already exists, skipping.`);
continue;
} catch (error) {
// File does not exist, proceed with generation
}
console.log(`Generating video for prompt: "${metadata.videoPrompt}"`);
const inputfolderFullpath = COMFY_OUTPUT_DIR.replace("output", "input");
await fs.copyFile(inputFile, path.join(inputfolderFullpath, file));
try {
await generateVideo(
metadata.videoPrompt,
file,
newFileName,
COMFY_BASE_URL,
COMFY_OUTPUT_DIR
);
console.log(`Successfully generated and saved ${newFileName}`);
} catch (error) {
console.error(`Error generating video for ${file}:`, error);
}
} else {
console.log(`Skipping ${file}, no valid videoPrompt metadata found.`);
}
}
}
main().catch(console.error);

View File

@ -0,0 +1,64 @@
import { callLmstudio } from '../lib/lmstudio';
import { logger } from '../lib/logger';
import dotenv from 'dotenv';
import { downloadImagesFromPinterestSearch } from '../lib/pinterest';
dotenv.config();
const PHOTOS_PER_KEYWORD = 10;
// Hard-coded user prompt
const HARDCODED_USER_PROMPT = process.env.HARDCODED_USER_PROMPT || `
Generate 20 keywords for various photogeneric product. List of 20 most common photo generic product :
Example output : ["food", "perfume", "accesory", "jewelry", "shoes"...]
`;
// Re-usable helper to extract JSON embedded in text
function extractJsonFromText(text: string): any | null {
if (!text || typeof text !== 'string') return null;
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (fenced && fenced[1]) {
try { return JSON.parse(fenced[1].trim()); } catch (e) { /* fall through */ }
}
const brace = text.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
if (brace && brace[0]) {
try { return JSON.parse(brace[0]); } catch (e) { return null; }
}
// Attempt line-separated keywords fallback
const lines = text.split(/\r?\n/).map((l: string) => l.trim()).filter(Boolean);
if (lines.length > 1) return lines;
return null;
}
(async () => {
logger.info(`Starting photo download process with prompt: "${HARDCODED_USER_PROMPT}"`);
// 1. Extract keywords from the hardcoded prompt
const keywords = ["fullbody portrait girl", "fullbody portrait 18y girl", "fullbody portrait cute girl", "fullbody portrait blond girl", "fullbody portrait 20y girl"];
if (!keywords || keywords.length === 0) {
logger.error("Could not extract keywords from prompt. Exiting.");
return;
}
logger.info(`Extracted keywords: ${keywords.join(', ')}`);
// 2. Search Pinterest for each keyword and download photos directly
let totalDownloads = 0;
for (const keyword of keywords) {
try {
logger.info(`Downloading photos for keyword: "${keyword}"`);
const downloadedPaths = await downloadImagesFromPinterestSearch(`${keyword}`, PHOTOS_PER_KEYWORD);
if (downloadedPaths.length > 0) {
logger.info(`Successfully downloaded ${downloadedPaths.length} images for "${keyword}"`);
totalDownloads += downloadedPaths.length;
} else {
logger.warn(`No images were downloaded for "${keyword}"`);
}
} catch (error) {
logger.error(`An error occurred while processing keyword ${keyword}:`, error);
}
}
logger.info(`Photo download process finished. Total images downloaded: ${totalDownloads}`);
})();

View File

@ -0,0 +1,82 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import dotenv from 'dotenv';
import { callLMStudioAPIWithFile, callLmstudio } from '../lib/lmstudio';
import { convertImage } from '../lib/image-converter';
import { logger } from '../lib/logger';
dotenv.config();
const COMFY_BASE_URL = process.env.SERVER2_COMFY_BASE_URL;
const COMFY_OUTPUT_DIR = process.env.SERVER2_COMFY_OUTPUT_DIR;
const INPUT_DIR = './input';
const GENERATED_DIR = './generated';
async function batchConvert() {
if (!COMFY_BASE_URL || !COMFY_OUTPUT_DIR) {
throw new Error('ComfyUI server details are not defined in the .env file');
}
try {
await fs.mkdir(GENERATED_DIR, { recursive: true });
const files = await fs.readdir(INPUT_DIR);
for (const file of files) {
try {
const inputFile = path.join(INPUT_DIR, file);
logger.info(`Processing ${inputFile}`);
const firstPrompt = `
Read the file in the photo and detect the main subject. Extract the following information:
- what is the main subject in one word
- describe character in 5 words
- describe background in 5 words
- 3 ideas to make this character's look, 2 or 3 words per idea
Return the result in this format:
{ "character":"", "characterDescription": "", "background":"", "idea":"idea1, idea2, idea3"}
`;
const imageInfo = await callLMStudioAPIWithFile(inputFile, firstPrompt);
const { character, idea, background } = imageInfo;
const ideas = idea.split(',').map((i: string) => i.trim());
const secondPrompt = `
Generate a prompt to convert the photo to a group dancing photo.
I need only the prompt in this format, main subject is ${character}
{"result":"
Change the camera angle to a full-body shot, place many ${character} in the scene messily,
Change lighting to silhouette lighting,
Change the style for each body using these ideas: ${ideas.join(', ')},
Change color of each body,
Change the background in horror movie style of ${background},
Overall photo should be realistic and spooky,
"} `;
const promptResult = await callLmstudio(secondPrompt);
const finalPrompt = promptResult.result;
const inputFolderFullPath = COMFY_OUTPUT_DIR.replace("output", "input");
const serverInputFile = path.join(inputFolderFullPath, file);
await fs.copyFile(inputFile, serverInputFile);
logger.info(`Generating image for ${file} with prompt: ${finalPrompt}`);
const generatedFile = await convertImage(finalPrompt, file, COMFY_BASE_URL, COMFY_OUTPUT_DIR);
const finalOutputPath = path.join(GENERATED_DIR, path.basename(generatedFile));
await fs.copyFile(generatedFile, finalOutputPath);
logger.info(`Saved converted image to ${finalOutputPath}`);
} catch (e) {
logger.error('An error occurred during batch conversion:', e);
continue;
}
}
} catch (error) {
logger.error('An error occurred during batch conversion:', error);
}
}
batchConvert();

View File

@ -0,0 +1,54 @@
import * as fs from 'fs';
import * as path from 'path';
import { callLMStudioAPIWithFile } from '../lib/lmstudio';
import { embedJsonToPng } from '../lib/util';
const imageDir = 'C:\\Users\\fm201\\Desktop\\vton\\bags';
async function processImages() {
try {
const files = fs.readdirSync(imageDir);
const imageFiles = files.filter(file => /\.(png)$/i.test(file));
for (const imageFile of imageFiles) {
const imagePath = path.join(imageDir, imageFile);
console.log(`Processing ${imagePath}...`);
const prompt = `
Based on the handbag in the image, generate 10 outfit prompts that would complement it.
Each prompt should be a short, descriptive sentence of around 20 words.
Return the result in the following JSON format:
{"result": ["outfit prompt 1", "outfit prompt 2", ...]}
`;
try {
const response = await callLMStudioAPIWithFile(imagePath, prompt);
let outfitPrompts;
if (typeof response === 'string') {
try {
outfitPrompts = JSON.parse(response);
} catch (e) {
console.error(`Failed to parse JSON string for ${imageFile}:`, response);
continue;
}
} else {
outfitPrompts = response;
}
if (outfitPrompts && outfitPrompts.result) {
await embedJsonToPng(imagePath, outfitPrompts);
console.log(`Successfully embedded prompts into ${imageFile}`);
} else {
console.error(`Invalid JSON response for ${imageFile}:`, response);
}
} catch (error) {
console.error(`Failed to process ${imageFile}:`, error);
}
}
} catch (error) {
console.error('Error reading image directory:', error);
}
}
processImages();

View File

@ -0,0 +1,122 @@
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import dotenv from 'dotenv';
import { callLMStudioAPIWithFile } from '../lib/lmstudio';
import { generateImage } from '../lib/image-generator';
import { convertImageWithFile } from '../lib/image-converter';
dotenv.config();
const inputDir = path.resolve('./input');
const outputDir = path.resolve('./generated');
async function main() {
const comfyBaseUrl = process.env.SERVER1_COMFY_BASE_URL;
const comfyOutputDir = process.env.SERVER1_COMFY_OUTPUT_DIR;
if (!comfyBaseUrl || !comfyOutputDir) {
throw new Error('SERVER1_COMFY_BASE_URL and SERVER1_COMFY_OUTPUT_DIR must be set in .env file');
}
const comfyInputDir = comfyOutputDir.replace("output", "input");
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const files = fs.readdirSync(inputDir);
for (const file of files) {
const imagePath = path.join(inputDir, file);
// Calculate file hash
const fileBuffer = fs.readFileSync(imagePath);
const hashSum = crypto.createHash('sha256');
hashSum.update(fileBuffer);
const hexHash = hashSum.digest('hex');
const genFileName = `${hexHash}_gen.png`;
const convertedFileName = `${hexHash}_converted.png`;
const genFilePath = path.join(outputDir, genFileName);
const convertedFilePath = path.join(outputDir, convertedFileName);
if (fs.existsSync(genFilePath) && fs.existsSync(convertedFilePath)) {
console.log(`Skipping ${file} as it has already been processed.`);
continue;
}
console.log(`Processing ${imagePath}...`);
// 1. Ask LLM to make a prompt for this photo
const llmPrompt = `
Extract the following information from the attached image: face detail, hairstyle, eye color, hair color.
Output instruction:
{ result: "face detail, hairstyle, eye color, hair color" }
`;
const extractedInfoJSON = await callLMStudioAPIWithFile(imagePath, llmPrompt);
if (!extractedInfoJSON) {
console.error(`Could not get prompt from LLM for ${imagePath}`);
continue;
}
const extractedInfo = extractedInfoJSON.result;
console.log(`LLM response: ${extractedInfo}`);
// 2. Generate photo using the prompt
const prefix = "Highly detailed face portrait of 20 years old girl, light gray background with faint gradient, look at camera in front";
const finalPrompt = prefix + extractedInfo;
console.log(`Generating image with prompt: ${finalPrompt}`);
const generatedImagePath = await generateImage(
finalPrompt,
genFileName,
comfyBaseUrl,
comfyOutputDir,
'flux'
);
if (generatedImagePath) {
console.log(`Generated image saved to: ${generatedImagePath}`);
// 3. Convert the image
const generatedImageFileName = path.basename(generatedImagePath);
const originalImageFileName = file;
const destGeneratedImagePath = path.join(comfyInputDir, generatedImageFileName);
const destOriginalImagePath = path.join(comfyInputDir, originalImageFileName);
// Copy files to comfy input directory
await fsPromises.copyFile(generatedImagePath, destGeneratedImagePath);
await fsPromises.copyFile(imagePath, destOriginalImagePath);
console.log(`Copied ${generatedImageFileName} and ${originalImageFileName} to ${comfyInputDir}`);
const conversionPrompt = "Replace the hairs of image1 with hairs of image2, change face of image1 with face of image2.";
const convertedResultPath = await convertImageWithFile(
conversionPrompt,
generatedImageFileName,
originalImageFileName,
comfyBaseUrl,
comfyOutputDir
);
if (convertedResultPath) {
// Rename the output file to the final converted name
await fsPromises.rename(convertedResultPath, convertedFilePath);
console.log(`Converted image saved to: ${convertedFilePath}`);
} else {
console.error(`Failed to convert image for ${originalImageFileName}`);
}
} else {
console.error(`Failed to generate image for prompt: ${finalPrompt}`);
}
}
}
main().catch(console.error);

View File

@ -0,0 +1,74 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { generateVideo } from '../lib/video-generator';
import dotenv from 'dotenv';
dotenv.config();
const inputFolder = './input';
const prompt = "a girl making a cute pose, static camera";
b
async function main() {
try {
const files = await fs.readdir(inputFolder);
const pngFiles = files.filter(file => path.extname(file).toLowerCase() === '.png');
if (pngFiles.length === 0) {
console.log('No PNG files found in the input folder.');
return;
}
const comfyBaseUrl = process.env.SERVER2_COMFY_BASE_URL;
const comfyOutputDir = process.env.SERVER2_COMFY_OUTPUT_DIR;
if (!comfyBaseUrl || !comfyOutputDir) {
console.error('Please define SERVER1_COMFY_BASE_URL and SERVER1_COMFY_OUTPUT_DIR in your .env file.');
return;
}
const comfyInputDir = comfyOutputDir.replace('output', 'input');
for (const file of pngFiles) {
const inputImagePath = path.join(inputFolder, file);
const comfyInputImagePath = path.join(comfyInputDir, file);
console.log(`Processing ${file}...`);
try {
await fs.access(inputImagePath);
// Copy file to comfy input directory
await fs.copyFile(inputImagePath, comfyInputImagePath);
console.log(`Copied ${file} to ComfyUI input directory.`);
} catch (e) {
console.error(`Error copying file ${file}:`, e);
continue;
}
const newFileName = `${path.parse(file).name}.mp4`;
try {
const generatedVideoPath = await generateVideo(
prompt,
file, // Pass only the filename as per instructions
newFileName,
comfyBaseUrl,
comfyOutputDir
);
console.log(`Successfully generated video for ${file} at: ${generatedVideoPath}`);
} catch (e: any) {
if (e.code === 'ECONNREFUSED' || e.code === 'ETIMEDOUT') {
console.error(`\nError: Connection to ComfyUI server at ${comfyBaseUrl} failed.`);
console.error('Please ensure the ComfyUI server is running and accessible.');
break;
} else {
console.error(`An error occurred while generating video for ${file}:`, e);
}
}
}
} catch (error) {
console.error('An unexpected error occurred:', error);
}
}
main();

View File

@ -0,0 +1,179 @@
import dotenv from 'dotenv';
import * as fs from 'fs/promises';
import * as path from 'path';
import { logger } from '../lib/logger';
import { getPinUrlFromPinterest, downloadImageFromPin } from '../lib/pinterest';
import { convertImage, extractCloth } from '../lib/image-converter';
dotenv.config();
const KEYWORDS = [
'teen skirt outfit',
'teen casual cute outfit',
'cute outfit',
'summer dress',
'elegant dress',
'bohemian dress',
'ball gown dress',
'cosplay outfit',
'vintage skirt outfit',
'casual skirt outfit',
'harajuku outfit',
'pop colorful outfit',
'cute colorful outfit',
'japanese idol outfit',
'folk dress outfit',
];
const TARGET_COUNT = Number(process.env.IMAGE_COUNT || 20);
const PROMPT =
`Change pose of the person in the image1 to stantind in front of camera, with a smile, full body visible, wearing a fashionable outfit suitable for a casual day out. The background should be a white. Ensure the lighting is bright and natural, highlighting the details of the outfit and the person's features.
`;
const PROMPT2 =
`Extract clothes from image1, put it on the light gray backgroud
`;
type ServerCfg = { baseUrl: string; outputDir: string; inputDir: string };
function getServerConfig(): ServerCfg {
const candidates = [
//{ baseUrl: process.env.SERVER1_COMFY_BASE_URL, outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR },
{ baseUrl: process.env.SERVER2_COMFY_BASE_URL, outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR },
].filter((s): s is { baseUrl: string; outputDir: string } => !!s.baseUrl && !!s.outputDir);
if (candidates.length === 0) {
throw new Error(
'No ComfyUI server configured. Please set SERVER1_COMFY_BASE_URL/OUTPUT_DIR or SERVER2_COMFY_BASE_URL/OUTPUT_DIR in .env'
);
}
const chosen = candidates[0];
const inputDir = chosen.outputDir.replace('output', 'input');
return { ...chosen, inputDir };
}
function sleep(ms: number) {
return new Promise((res) => setTimeout(res, ms));
}
async function ensureDir(p: string) {
try {
await fs.mkdir(p, { recursive: true });
} catch {
// ignore
}
}
async function collectImages(keyword: string, total: number): Promise<string[]> {
const results: string[] = [];
// ensure local download dir exists (pinterest.ts also ensures it, but harmless here)
await ensureDir(path.join(process.cwd(), 'download'));
while (results.length < total) {
try {
const pinUrl = await getPinUrlFromPinterest(keyword);
if (!pinUrl) {
logger.warn('No pin URL found, retrying...');
await sleep(1500);
continue;
}
const remaining = total - results.length;
// attempt to grab up to 5 per pin to reduce churn
const batchTarget = Math.min(5, remaining);
const imgs = await downloadImageFromPin(pinUrl, batchTarget);
if (imgs && imgs.length > 0) {
results.push(...imgs);
logger.info(`Downloaded ${imgs.length} image(s) from ${pinUrl}.Progress: ${results.length}/${total}`);
} else {
logger.warn(`Pin yielded no downloadable images: ${pinUrl}`);
}
await sleep(1000 + Math.random() * 1000);
} catch (err) {
logger.error('Error while collecting images:', err);
await sleep(2000);
}
}
return results.slice(0, total);
}
async function processImages(imagePaths: string[], server: ServerCfg) {
await ensureDir(server.inputDir);
for (const localImagePath of imagePaths) {
const baseName = path.basename(localImagePath);
const serverInputPath = path.join(server.inputDir, baseName);
try {
// copy source image into ComfyUI input dir so workflow node can find it by filename
await fs.copyFile(localImagePath, serverInputPath);
logger.info(`Copied ${localImagePath} -> ${serverInputPath}`);
// Run conversion (sequential to avoid output race conditions)
const generatedPath1 = await extractCloth(
PROMPT2,
baseName,
server.baseUrl,
server.outputDir,
{ width: 720, height: 1280 } // portrait
);
/*
await fs.copyFile(generatedPath1, serverInputPath);
const baseName2 = path.basename(localImagePath);
const generatedPath2 = await extractCloth(
PROMPT2,
baseName2,
server.baseUrl,
server.outputDir,
{ width: 720, height: 1280 } // portrait
);
*/
logger.info(`Generated image: ${generatedPath1}`);
} catch (err) {
logger.error(`Failed to convert ${localImagePath}:`, err);
} finally {
// cleanup both server input copy and local downloaded file
try {
await fs.unlink(serverInputPath);
} catch { }
try {
await fs.unlink(localImagePath);
} catch { }
await sleep(500);
}
}
}
async function main() {
const server = getServerConfig();
// Infinite loop as requested
while (true) {
for (const KEYWORD of KEYWORDS) {
logger.info(`Starting Pinterest image conversion loop for keyword: "${KEYWORD}" (target ${TARGET_COUNT})`);
try {
const images = await collectImages(KEYWORD, TARGET_COUNT);
logger.info(`Collected ${images.length} image(s). Starting conversion...`);
await processImages(images, server);
logger.info('Iteration completed. Sleeping before next cycle...');
} catch (err) {
logger.error('Iteration failed:', err);
}
// brief pause between iterations
await sleep(10000);
}
}
}
process.on('unhandledRejection', (reason) => {
logger.error('UnhandledRejection:', reason);
});
process.on('uncaughtException', (err) => {
logger.error('UncaughtException:', err);
});
main().catch((e) => {
logger.error('Fatal error:', e);
});

View File

@ -0,0 +1,247 @@
import dotenv from 'dotenv';
import * as fs from 'fs/promises';
import * as path from 'path';
import { logger } from '../lib/logger';
import { getPinUrlFromPinterest, downloadImageFromPin } from '../lib/pinterest';
import { generateImage } from '../lib/image-generator-face';
import { callLMStudioAPIWithFile } from '../lib/lmstudio';
dotenv.config();
const MODE: "keywords" | "pinIds" = "keywords";
const KEYWORDS = [
'a girl in scary forest',
'a girl in graveyard',
''];
const pinIDs = [
"22377329393970367",
"18999629674210230",
"3166662232983784",
"291537775902572408",
"2744449769232655",
"9429480465939847",
"34058540926328062",
"1071153092617107265",
"6825836928646465",
"1407443629997072",
"333407178685095962",
"15833036184288417",
"6825836928284784",
"2181499815469509",
"199706564723106062",
"1759287348280571",
"56083957854040032",
"3025924743999802",
"2955556001576084",
"1407443627212889",
"836965911982723974",
"97460779431981493",
"282600945363725869",
"/59532026387104913",
"70437490453979",
"152489137384620437",
"50947039528553588",
"73042825197955754",
"624593042089280419",
"351912466315529",
"624030092104188250",
"21673641951379251",
"27021666506512503",
"3377768467678091",
"985231163409578",
"17240411068654164"
]
const TARGET_COUNT = Number(process.env.IMAGE_COUNT || 20);
const PROMPT =
`
change camera angle to fullbody shot from image1,
change background to light gray with faing gradient,
change clothes to shite sports bra and shite cotton short pants
`;
const LmStudioPrompt = `
describe the image in 50 words including, scene, lighting, describe character(s), add some beautiful accent like light, fog, starts, lamps, whatever suits with scene.
then return in this format
{"prompt":""}
`;
type ServerCfg = { baseUrl: string; outputDir: string; inputDir: string };
function getServerConfig(): ServerCfg {
const candidates = [
//{ baseUrl: process.env.SERVER1_COMFY_BASE_URL, outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR },
{ baseUrl: process.env.SERVER2_COMFY_BASE_URL, outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR },
].filter((s): s is { baseUrl: string; outputDir: string } => !!s.baseUrl && !!s.outputDir);
if (candidates.length === 0) {
throw new Error(
'No ComfyUI server configured. Please set SERVER1_COMFY_BASE_URL/OUTPUT_DIR or SERVER2_COMFY_BASE_URL/OUTPUT_DIR in .env'
);
}
const chosen = candidates[0];
const inputDir = chosen.outputDir.replace('output', 'input');
return { ...chosen, inputDir };
}
function sleep(ms: number) {
return new Promise((res) => setTimeout(res, ms));
}
async function ensureDir(p: string) {
try {
await fs.mkdir(p, { recursive: true });
} catch {
// ignore
}
}
async function collectImages(keyword: string, total: number): Promise<string[]> {
const results: string[] = [];
// ensure local download dir exists (pinterest.ts also ensures it, but harmless here)
await ensureDir(path.join(process.cwd(), 'download'));
if (MODE == "keywords") {
while (results.length < total) {
try {
const pinUrl = await getPinUrlFromPinterest(keyword);
if (!pinUrl) {
logger.warn('No pin URL found, retrying...');
await sleep(1500);
return [];
}
const remaining = total - results.length;
// attempt to grab up to 5 per pin to reduce churn
const batchTarget = Math.min(5, remaining);
const imgs = await downloadImageFromPin(pinUrl, batchTarget);
if (imgs && imgs.length > 0) {
results.push(...imgs);
logger.info(`Downloaded ${imgs.length} image(s) from ${pinUrl}.Progress: ${results.length}/${total}`);
} else {
logger.warn(`Pin yielded no downloadable images: ${pinUrl}`);
}
await sleep(1000 + Math.random() * 1000);
} catch (err) {
logger.error('Error while collecting images:', err);
await sleep(2000);
}
}
} else if (MODE == "pinIds") {
while (results.length < total) {
const shuffledPinIds = pinIDs.slice().sort(() => 0.5 - Math.random());
const pinUrl = `https://www.pinterest.com/pin/${shuffledPinIds[0]}`;
try {
const remaining = total - results.length;
// attempt to grab up to 5 per pin to reduce churn
const batchTarget = Math.min(5, remaining);
const imgs = await downloadImageFromPin(pinUrl, batchTarget);
if (imgs && imgs.length > 0) {
results.push(...imgs);
logger.info(`Downloaded ${imgs.length} image(s) from ${pinUrl}.Progress: ${results.length}/${total}`);
} else {
logger.warn(`Pin yielded no downloadable images: ${pinUrl}`);
}
await sleep(1000 + Math.random() * 1000);
} catch (err) {
logger.error('Error while collecting images:', err);
await sleep(2000);
}
}
}
return results.slice(0, total);
}
let imageIndex = 0;
async function processImages(imagePaths: string[], server: ServerCfg) {
await ensureDir(server.inputDir);
for (const localImagePath of imagePaths) {
const response = await callLMStudioAPIWithFile(localImagePath, LmStudioPrompt);
const prompt = response.prompt;
const baseName = path.basename(localImagePath);
const serverInputPath = path.join(server.inputDir, baseName);
try {
// copy source image into ComfyUI input dir so workflow node can find it by filename
await fs.copyFile(localImagePath, serverInputPath);
logger.info(`Copied ${localImagePath} -> ${serverInputPath}`);
// Run conversion (sequential to avoid output race conditions)
const generatedPath = await generateImage(
`ultra realistic photo, ${prompt}`,
baseName,
`monster_${imageIndex}.png`,
server.baseUrl,
server.outputDir,
{ width: 1280, height: 720 } // portrait
);
logger.info(`Generated image: ${generatedPath}`);
imageIndex++;
} catch (err) {
logger.error(`Failed to convert ${localImagePath}:`, err);
} finally {
// cleanup both server input copy and local downloaded file
try {
await fs.unlink(serverInputPath);
} catch { }
try {
await fs.unlink(localImagePath);
} catch { }
await sleep(500);
}
}
}
async function main() {
const server = getServerConfig();
const files = await fs.readdir(path.join(process.cwd(), 'generated'));
const monsterFiles = files.filter((f) => f.startsWith('monster_'));
if (monsterFiles.length > 0) {
const latestFile = monsterFiles.sort().pop();
if (latestFile) {
const latestIndex = parseInt(latestFile.replace('monster_', '').replace('.png', ''));
imageIndex = latestIndex + 1;
}
}
// Infinite loop as requested
while (true) {
for (const KEYWORD of KEYWORDS) {
logger.info(`Starting Pinterest image conversion loop for keyword: "${KEYWORD}" (target ${TARGET_COUNT})`);
try {
const images = await collectImages(KEYWORD, TARGET_COUNT);
logger.info(`Collected ${images.length} image(s). Starting conversion...`);
if (images.length == 0)
continue;
await processImages(images, server);
logger.info('Iteration completed. Sleeping before next cycle...');
} catch (err) {
logger.error('Iteration failed:', err);
}
// brief pause between iterations
await sleep(10000);
}
}
}
process.on('unhandledRejection', (reason) => {
logger.error('UnhandledRejection:', reason);
});
process.on('uncaughtException', (err) => {
logger.error('UncaughtException:', err);
});
main().catch((e) => {
logger.error('Fatal error:', e);
});

156
src/tools/vton_generator.ts Normal file
View File

@ -0,0 +1,156 @@
import * as fs from 'fs';
import * as path from 'path';
import { convertImageVton, convertImage } from '../lib/image-converter';
import * as dotenv from 'dotenv';
import sharp from 'sharp';
dotenv.config();
const clothesDir = 'D:\\projects\\random_video_maker\\input';
const outputDir = 'generated';
const comfyBaseUrl = process.env.SERVER1_COMFY_BASE_URL;
const comfyOutputDir = process.env.SERVER1_COMFY_OUTPUT_DIR;
function getNextIndex(directory: string): number {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
return 0;
}
const dirs = fs.readdirSync(directory, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
const vtonDirs = dirs.filter(dir => dir.startsWith('vton_'));
if (vtonDirs.length === 0) {
return 0;
}
const indices = vtonDirs.map(dir => {
const match = dir.match(/vton_(\d+)/);
return match ? parseInt(match[1], 10) : -1;
});
return Math.max(...indices) + 1;
}
function getRandomFile(directory: string): string {
const files = fs.readdirSync(directory).filter(file => /\.(jpg|png|jpeg)$/i.test(file));
if (files.length === 0) {
throw new Error(`No image files found in directory: ${directory}`);
}
const randomFile = files[Math.floor(Math.random() * files.length)];
return path.join(directory, randomFile);
}
async function generateVtonImages() {
if (!comfyBaseUrl || !comfyOutputDir) {
throw new Error("ComfyUI URL or Output Directory is not set in environment variables.");
}
let index = getNextIndex(outputDir);
const comfyInputDir = comfyOutputDir.replace("output", "input");
while (true) { // Infinite loop
const iterationDir = path.join(outputDir, `vton_${index}`);
fs.mkdirSync(iterationDir, { recursive: true });
try {
const personOrigPath = getRandomFile(clothesDir);
const clothOrigPath = getRandomFile(clothesDir);
fs.copyFileSync(personOrigPath, path.join(iterationDir, '1-personOrig.png'));
fs.copyFileSync(clothOrigPath, path.join(iterationDir, '3-clothOrig.png'));
const personOrigFileName = path.basename(personOrigPath);
const clothOrigFileName = path.basename(clothOrigPath);
fs.copyFileSync(personOrigPath, path.join(comfyInputDir, personOrigFileName));
fs.copyFileSync(clothOrigPath, path.join(comfyInputDir, clothOrigFileName));
console.log(`Processing person: ${personOrigPath}, cloth: ${clothOrigPath}`);
const cleanePersonImagePath = await convertImage("请把姿势改成站立的,转换成全身照片。去掉衣服,只保留白色运动文胸和白色短裤。双脚保持赤脚。背景为浅灰色。", personOrigFileName, comfyBaseUrl, comfyOutputDir, { width: 720, height: 1280 });
fs.copyFileSync(cleanePersonImagePath, path.join(iterationDir, '2-personCleaned.png'));
const cleanedPersonFileName = path.basename(cleanePersonImagePath);
fs.copyFileSync(cleanePersonImagePath, path.join(comfyInputDir, cleanedPersonFileName));
const cleanedClothImagePath = await convertImage("请将图1中的上衣、下装和配饰分别提取出来放到同一个浅灰色的背景上。", clothOrigFileName, comfyBaseUrl, comfyOutputDir, { width: 720, height: 1280 });
fs.copyFileSync(cleanedClothImagePath, path.join(iterationDir, '4-clothCleaned.png'));
const cleanedClothFileName = path.basename(cleanedClothImagePath);
fs.copyFileSync(cleanedClothImagePath, path.join(comfyInputDir, cleanedClothFileName));
const outputFilename = `vton_final_${index}.png`;
const generatedImagePath = await convertImageVton(cleanedPersonFileName, cleanedClothFileName, outputFilename, comfyBaseUrl, comfyOutputDir, { width: 720, height: 1280 });
if (generatedImagePath) {
fs.copyFileSync(generatedImagePath, path.join(iterationDir, '5-finalResult.png'));
console.log(`Generated image saved to ${generatedImagePath}`);
// --- Create composite image ---
const imagePaths = [
path.join(iterationDir, '1-personOrig.png'),
path.join(iterationDir, '3-clothOrig.png'),
path.join(iterationDir, '2-personCleaned.png'),
path.join(iterationDir, '4-clothCleaned.png'),
path.join(iterationDir, '5-finalResult.png')
];
const resizedImages = [];
let totalWidth = 10; // Initial left margin
const resizedHeight = 720;
for (const imagePath of imagePaths) {
const image = sharp(imagePath);
const metadata = await image.metadata();
if (!metadata.width || !metadata.height) {
throw new Error(`Could not get metadata for image ${imagePath}`);
}
const resizedWidth = Math.round((metadata.width / metadata.height) * resizedHeight);
const resizedImageBuffer = await image.resize(resizedWidth, resizedHeight).toBuffer();
resizedImages.push({
buffer: resizedImageBuffer,
width: resizedWidth
});
totalWidth += resizedWidth + 10; // Add image width and right margin
}
const compositeOps = [];
let currentLeft = 10; // Start with left margin
for (const img of resizedImages) {
compositeOps.push({
input: img.buffer,
top: 10, // 10px top margin
left: currentLeft
});
currentLeft += img.width + 10; // Move to the next position
}
await sharp({
create: {
width: totalWidth,
height: 740,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 }
}
})
.composite(compositeOps)
.toFile(path.join(iterationDir, 'process.png'));
console.log(`Generated composite image process.png in ${iterationDir}`);
// --- End of composite image creation ---
index++;
} else {
console.error(`Failed to generate image for index ${index}`);
}
} catch (error) {
console.error("An error occurred during image generation:", error);
// Optional: wait for a bit before retrying to avoid spamming errors
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
generateVtonImages().catch(console.error);

View File

@ -0,0 +1,538 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import dotenv from 'dotenv';
import { downloadImagesFromPinterestSearch } from '../lib/pinterest';
import { convertImage, convertImageWithFile, convertImageWithFileForPose, convertImageWithFileHandbag } from '../lib/image-converter';
import { logger } from '../lib/logger';
import { callLmstudio, callLMStudioAPIWithFile } from '../lib/lmstudio';
import { upscale } from '../lib/image-upscaler';
dotenv.config();
const SERVER1_COMFY_BASE_URL = process.env.SERVER1_COMFY_BASE_URL!;
const SERVER1_COMFY_OUTPUT_DIR = process.env.SERVER1_COMFY_OUTPUT_DIR!;
const imageSize: { width: number; height: number } = { width: 1280, height: 720 };
async function upscaleAndFix(
baseImage: string,
faceImage: string,
outputFilename: string,
outputDir: string,
): Promise<void> {
try {
// Copy both images to ComfyUI input directory
const inputFolderFullPath = SERVER1_COMFY_OUTPUT_DIR.replace('output', 'input');
await fs.mkdir(inputFolderFullPath, { recursive: true });
const baseFilePath = path.join(outputDir, baseImage);
const referenceFilePath = path.join(outputDir, faceImage);
const baseFileName = path.basename(baseImage);
const referenceFileName = path.basename(faceImage);
const inputBasePath = path.join(inputFolderFullPath, baseFileName);
const inputReferencePath = path.join(inputFolderFullPath, referenceFileName);
logger.info(`Copying base image to ComfyUI input: ${inputBasePath}`);
await fs.copyFile(baseFilePath, inputBasePath);
logger.info(`Copying reference image to ComfyUI input: ${inputReferencePath}`);
await fs.copyFile(referenceFilePath, inputReferencePath);
// Convert images with prompt
logger.info(`Processing images with convertImageWithFile...`);
const convertedImagePath = await upscale(
baseFileName,
SERVER1_COMFY_BASE_URL,
SERVER1_COMFY_OUTPUT_DIR,
)
logger.info(`Converted image: ${convertedImagePath}`);
// Copy the converted image to final destination
const finalOutputPath = path.join(outputDir, outputFilename);
logger.info(`Copying to final destination: ${finalOutputPath}`);
await fs.copyFile(convertedImagePath, finalOutputPath);
logger.info(`✓ Successfully generated: ${finalOutputPath}`);
} catch (error) {
logger.error(`Error processing two images:`, error);
throw error;
}
}
/**
* Process a single image: download from Pinterest, convert with prompt, and save
* @param keyword - Pinterest search keyword
* @param prompt - Image conversion prompt
* @param filename - Output filename
* @param outputDir - Directory to save the generated file
* @param shouldConvert - Whether to convert the image with prompt or just copy it
*/
async function processImage(
keyword: string,
prompt: string,
filename: string,
outputDir: string,
shouldConvert: boolean = true
): Promise<void> {
try {
logger.info(`\n=== Processing: ${filename} ===`);
logger.info(`Keyword: ${keyword}`);
logger.info(`Should convert: ${shouldConvert}`);
// Step 1: Download image from Pinterest
logger.info(`Step 1: Downloading image from Pinterest with keyword: "${keyword}"...`);
const downloadedImages = await downloadImagesFromPinterestSearch(keyword, 1);
if (downloadedImages.length === 0) {
logger.error(`Failed to download image for keyword: "${keyword}"`);
return;
}
const downloadedImagePath = downloadedImages[0];
logger.info(`Downloaded image: ${downloadedImagePath}`);
const finalOutputPath = path.join(outputDir, filename);
if (shouldConvert) {
logger.info(`Prompt: ${prompt}`);
// Step 2: Copy image to ComfyUI input directory
const inputFolderFullPath = SERVER1_COMFY_OUTPUT_DIR.replace('output', 'input');
await fs.mkdir(inputFolderFullPath, { recursive: true });
const imageFileName = path.basename(downloadedImagePath);
const inputImagePath = path.join(inputFolderFullPath, imageFileName);
logger.info(`Step 2: Copying image to ComfyUI input folder: ${inputImagePath}`);
await fs.copyFile(downloadedImagePath, inputImagePath);
// Step 3: Convert image with prompt
logger.info(`Step 3: Converting image with prompt...`);
const convertedImagePath = await convertImage(
prompt,
imageFileName,
SERVER1_COMFY_BASE_URL,
SERVER1_COMFY_OUTPUT_DIR,
imageSize
);
logger.info(`Converted image: ${convertedImagePath}`);
// Step 4: Copy the converted image to final destination
logger.info(`Step 4: Copying to final destination: ${finalOutputPath}`);
await fs.copyFile(convertedImagePath, finalOutputPath);
} else {
// Just copy the downloaded image directly to the output directory with the specified filename
logger.info(`Step 2: Copying directly to final destination: ${finalOutputPath}`);
await fs.copyFile(downloadedImagePath, finalOutputPath);
}
logger.info(`✓ Successfully generated: ${finalOutputPath}`);
} catch (error) {
logger.error(`Error processing image for keyword "${keyword}":`, error);
throw error;
}
}
/**
* Convert an existing image with a prompt
* @param prompt - Image conversion prompt
* @param imagePath - Path to the existing image
* @param outputFilename - Output filename
* @param outputDir - Directory to save the converted file
*/
async function convertImageWithPrompt(
prompt: string,
imagePath: string,
outputFilename: string,
outputDir: string
): Promise<void> {
try {
logger.info(`\n=== Converting Image: ${outputFilename} ===`);
logger.info(`Source: ${imagePath}`);
logger.info(`Prompt: ${prompt}`);
// Step 1: Copy image to ComfyUI input directory
const inputFolderFullPath = SERVER1_COMFY_OUTPUT_DIR.replace('output', 'input');
await fs.mkdir(inputFolderFullPath, { recursive: true });
const imageFileName = path.basename(imagePath);
const inputImagePath = path.join(inputFolderFullPath, imageFileName);
logger.info(`Step 1: Copying image to ComfyUI input folder: ${inputImagePath}`);
await fs.copyFile(imagePath, inputImagePath);
// Step 2: Convert image with prompt
logger.info(`Step 2: Converting image with prompt...`);
const convertedImagePath = await convertImage(
prompt,
imageFileName,
SERVER1_COMFY_BASE_URL,
SERVER1_COMFY_OUTPUT_DIR,
imageSize
);
logger.info(`Converted image: ${convertedImagePath}`);
// Step 3: Copy the converted image to final destination
const finalOutputPath = path.join(outputDir, outputFilename);
logger.info(`Step 3: Copying to final destination: ${finalOutputPath}`);
await fs.copyFile(convertedImagePath, finalOutputPath);
logger.info(`✓ Successfully converted: ${finalOutputPath}`);
} catch (error) {
logger.error(`Error converting image:`, error);
throw error;
}
}
/**
* Process two images together: combine base image with reference image using prompt
* @param prompt - Processing prompt
* @param baseFile - Base image filename (in generated folder)
* @param referenceFile - Reference image filename (in generated folder)
* @param outputFilename - Output filename
* @param outputDir - Directory to save the generated file
*/
async function processTwoImages(
prompt: string,
baseFile: string,
referenceFile: string,
outputFilename: string,
outputDir: string,
isPose: boolean = false
): Promise<void> {
try {
logger.info(`\n=== Processing: ${outputFilename} ===`);
logger.info(`Base: ${baseFile}, Reference: ${referenceFile}`);
logger.info(`Prompt: ${prompt}`);
// Copy both images to ComfyUI input directory
const inputFolderFullPath = SERVER1_COMFY_OUTPUT_DIR.replace('output', 'input');
await fs.mkdir(inputFolderFullPath, { recursive: true });
const baseFilePath = path.join(outputDir, baseFile);
const referenceFilePath = path.join(outputDir, referenceFile);
const baseFileName = path.basename(baseFile);
const referenceFileName = path.basename(referenceFile);
const inputBasePath = path.join(inputFolderFullPath, baseFileName);
const inputReferencePath = path.join(inputFolderFullPath, referenceFileName);
logger.info(`Copying base image to ComfyUI input: ${inputBasePath}`);
await fs.copyFile(baseFilePath, inputBasePath);
logger.info(`Copying reference image to ComfyUI input: ${inputReferencePath}`);
await fs.copyFile(referenceFilePath, inputReferencePath);
// Convert images with prompt
logger.info(`Processing images with convertImageWithFile...`);
const convertedImagePath = isPose ? await convertImageWithFileForPose(
prompt,
baseFileName,
referenceFileName,
SERVER1_COMFY_BASE_URL,
SERVER1_COMFY_OUTPUT_DIR,
imageSize
) : await convertImageWithFile(
prompt,
baseFileName,
referenceFileName,
SERVER1_COMFY_BASE_URL,
SERVER1_COMFY_OUTPUT_DIR,
imageSize
)
logger.info(`Converted image: ${convertedImagePath}`);
// Copy the converted image to final destination
const finalOutputPath = path.join(outputDir, outputFilename);
logger.info(`Copying to final destination: ${finalOutputPath}`);
await fs.copyFile(convertedImagePath, finalOutputPath);
logger.info(`✓ Successfully generated: ${finalOutputPath}`);
} catch (error) {
logger.error(`Error processing two images:`, error);
throw error;
}
}
/**
* Process two images together: combine base image with reference image using prompt
* @param prompt - Processing prompt
* @param baseFile - Base image filename (in generated folder)
* @param referenceFile - Reference image filename (in generated folder)
* @param outputFilename - Output filename
* @param outputDir - Directory to save the generated file
*/
async function processTwoImagesHandbag(
prompt: string,
baseFile: string,
referenceFile: string,
outputFilename: string,
outputDir: string,
): Promise<void> {
try {
logger.info(`\n=== Processing: ${outputFilename} ===`);
logger.info(`Base: ${baseFile}, Reference: ${referenceFile}`);
logger.info(`Prompt: ${prompt}`);
// Copy both images to ComfyUI input directory
const inputFolderFullPath = SERVER1_COMFY_OUTPUT_DIR.replace('output', 'input');
await fs.mkdir(inputFolderFullPath, { recursive: true });
const baseFilePath = path.join(outputDir, baseFile);
const referenceFilePath = path.join(outputDir, referenceFile);
const baseFileName = path.basename(baseFile);
const referenceFileName = path.basename(referenceFile);
const inputBasePath = path.join(inputFolderFullPath, baseFileName);
const inputReferencePath = path.join(inputFolderFullPath, referenceFileName);
logger.info(`Copying base image to ComfyUI input: ${inputBasePath}`);
await fs.copyFile(baseFilePath, inputBasePath);
logger.info(`Copying reference image to ComfyUI input: ${inputReferencePath}`);
await fs.copyFile(referenceFilePath, inputReferencePath);
// Convert images with prompt
logger.info(`Processing images with convertImageWithFile...`);
const convertedImagePath = await convertImageWithFileHandbag(
prompt,
baseFileName,
referenceFileName,
SERVER1_COMFY_BASE_URL,
SERVER1_COMFY_OUTPUT_DIR,
imageSize
)
logger.info(`Converted image: ${convertedImagePath}`);
// Copy the converted image to final destination
const finalOutputPath = path.join(outputDir, outputFilename);
logger.info(`Copying to final destination: ${finalOutputPath}`);
await fs.copyFile(convertedImagePath, finalOutputPath);
logger.info(`✓ Successfully generated: ${finalOutputPath}`);
} catch (error) {
logger.error(`Error processing two images:`, error);
throw error;
}
}
/**
* Process a complete iteration: download base images and apply sequential transformations
*/
async function processIteration(iteration: number): Promise<void> {
try {
const timestamp = Date.now();
logger.info(`\n${'='.repeat(80)}`);
logger.info(`ITERATION ${iteration} - Starting with timestamp: ${timestamp}`);
logger.info(`${'='.repeat(80)}`);
// Create output directory for this iteration
const outputDir = path.join(process.cwd(), 'generated', `vton_${timestamp}`);
await fs.mkdir(outputDir, { recursive: true });
logger.info(`Output directory created: ${outputDir}`);
// === PHASE 1: Download base images ===
logger.info(`\n--- PHASE 1: Downloading base images ---`);
await processImage(
'cute girl face high resolution',
'',
`model_${timestamp}.png`,
outputDir,
false
);
await processImage(
'woman elegant outfit fullbody single',
'',
`outfit_${timestamp}.png`,
outputDir,
false
);
await processImage(
'photo elegant indoor room',
'',
`room_${timestamp}.png`,
outputDir,
false
);
await processImage(
'handbag single product photography',
'请提取照片中的包,并将其正面朝向地放置在亮灰色背景上。',
`handbag_${timestamp}.png`,
outputDir,
true
);
await processImage(
'woman portrait standing',
'',
`pose_${timestamp}.png`,
outputDir,
false
);
// === PHASE 2: Sequential transformations ===
logger.info(`\n--- PHASE 2: Sequential transformations ---`);
// Step 1: Generate outfit prompt using LMStudio API
logger.info('Step 1: Generating outfit prompt with LMStudio API...');
const outfitImagePath = path.join(outputDir, `outfit_${timestamp}.png`);
const outfitPromptResponse = await callLMStudioAPIWithFile(
outfitImagePath,
'Describe this outfit in detail about 30 words. Focus on color and cloth type. Return the result in this format: {"result":""}'
);
const outfitPrompt = outfitPromptResponse.result || outfitPromptResponse;
logger.info(`Generated outfit prompt: ${outfitPrompt}`);
// Step 2: Generate location prompt using LMStudio API
logger.info('Step 2: Generating location prompt with LMStudio API...');
const roomImagePath = path.join(outputDir, `room_${timestamp}.png`);
const locationPromptResponse = await callLMStudioAPIWithFile(
roomImagePath,
'Describe this location/room in detail about 30 words. Return the result in this format: {"result":""}'
);
const locationPrompt = locationPromptResponse.result || locationPromptResponse;
logger.info(`Generated location prompt: ${locationPrompt}`);
// Step 3: Generate Chinese prompt using LMStudio API
logger.info('Step 3: Generating Chinese prompt for model transformation...');
const chinesePromptRequest = `Generate a Chinese prompt for image transformation that describes:
- Prefix: genereate a portarit photo of a woman in image1
- Use outfit to: ${outfitPrompt}
- Use location to: ${locationPrompt}
Return the result in this format: {"result":""}`;
const chinesePromptResponse = await callLmstudio(chinesePromptRequest);
const chinesePrompt = chinesePromptResponse.result || chinesePromptResponse;
logger.info(`Generated Chinese prompt: ${chinesePrompt}`);
// Process model with outfit and location using the Chinese prompt
logger.info('Step 4: Processing model with outfit and location...');
const modelImagePath = path.join(outputDir, `model_${timestamp}.png`);
// Copy model image to ComfyUI input directory
const inputFolderFullPath = SERVER1_COMFY_OUTPUT_DIR.replace('output', 'input');
await fs.mkdir(inputFolderFullPath, { recursive: true });
const modelFileName = path.basename(modelImagePath);
const inputModelPath = path.join(inputFolderFullPath, modelFileName);
await fs.copyFile(modelImagePath, inputModelPath);
// Convert image with Chinese prompt and pose
await processTwoImages(
`请将图1中模特的姿势更改为图2的姿势。, ${chinesePrompt}`,
modelFileName,
`pose_${timestamp}.png`,
`model_outfit_location_pose_${timestamp}.png`,
outputDir,
true
);
// Step 5: Add handbag to model
await processTwoImagesHandbag(
'请将图1中的女性修改成手持图2的包。',
`model_outfit_location_pose_${timestamp}.png`,
`handbag_${timestamp}.png`,
`model_outfit_location_handbag1_${timestamp}.png`,
outputDir
);
await processTwoImagesHandbag(
'请让图1的女性看起来像是在手里拿着图2的包。',
`model_outfit_location_pose_${timestamp}.png`,
`handbag_${timestamp}.png`,
`model_outfit_location_handbag2_${timestamp}.png`,
outputDir
);
await processTwoImagesHandbag(
'请将图1中的女性修改成双手拿着图2的包。',
`model_outfit_location_pose_${timestamp}.png`,
`handbag_${timestamp}.png`,
`model_outfit_location_handbag3_${timestamp}.png`,
outputDir
);
await upscaleAndFix(
`model_outfit_location_handbag1_${timestamp}.png`,
`model_${timestamp}.png`,
`model_outfit_location_handbag1_upscaled_${timestamp}.png`,
outputDir
);
await upscaleAndFix(
`model_outfit_location_handbag2_${timestamp}.png`,
`model_${timestamp}.png`,
`model_outfit_location_handbag2_upscaled_${timestamp}.png`,
outputDir
);
await upscaleAndFix(
`model_outfit_location_handbag3_${timestamp}.png`,
`model_${timestamp}.png`,
`model_outfit_location_handbag3_upscaled_${timestamp}.png`,
outputDir
);
logger.info(`\n${'='.repeat(80)}`);
logger.info(`ITERATION ${iteration} COMPLETED!`);
logger.info(`Generated files are saved in: ${outputDir}`);
logger.info(`${'='.repeat(80)}\n`);
} catch (error) {
logger.error(`Error in iteration ${iteration}:`, error);
throw error;
}
}
/**
* Main execution function with infinite iteration
*/
async function main() {
let iteration = 1;
try {
logger.info('Starting infinite processing loop...');
logger.info('Press Ctrl+C to stop the process\n');
while (true) {
await processIteration(iteration);
iteration++;
// Small delay between iterations
logger.info('Waiting 5 seconds before next iteration...\n');
await new Promise(resolve => setTimeout(resolve, 5000));
}
} catch (error) {
logger.error('Error in main execution:', error);
process.exit(1);
}
}
// Execute main function if this file is run directly
if (require.main === module) {
main();
}
export { processImage, convertImageWithPrompt, processTwoImages, processIteration, main };