diff --git a/generateWorkflow.ts b/generateWorkflow.ts new file mode 100644 index 0000000..1543a6e --- /dev/null +++ b/generateWorkflow.ts @@ -0,0 +1,58 @@ +import Anthropic from "@anthropic-ai/sdk"; +import fs from "node:fs/promises"; +import path from "node:path"; +import assert from "node:assert"; + +// Looks for api key in envvar ANTHROPIC_API_KEY +assert(process.env.ANTHROPIC_API_KEY, "ANTHROPIC_API_KEY envvar not set"); +const anthropic = new Anthropic(); + +async function generateWorkflow(input: string): Promise { + const msg = await anthropic.messages.create( + { + model: "claude-3-5-sonnet-20240620", + max_tokens: 8192, + temperature: 0, + system: + 'Your job is to convert a json workflow graph for ai image generation into a typescript function. You should define a type for the input, using Zod for validation. You should use `.describe` to describe each parameter to the best of your ability. filename prefix is always set by the system in a different location. Do not extrapolate enum values. Always take the checkpoint value from config and types as demonstrated. Use snake_case for multi-word parameters. Only output the typescript, with no additional commentary. Here is an example output:\n\n```typescript\nimport { z } from "zod";\nimport { ComfyNode, Workflow } from "../../types";\nimport config from "../../config";\n\nlet checkpoint: any = config.models.checkpoints.enum.optional();\nif (config.warmupCkpt) {\n checkpoint = checkpoint.default(config.warmupCkpt);\n}\n\nconst RequestSchema = z.object({\n prompt: z.string().describe("The positive prompt for image generation"),\n negative_prompt: z\n .string()\n .optional()\n .describe("The negative prompt for image generation"),\n width: z\n .number()\n .int()\n .min(256)\n .max(2048)\n .optional()\n .default(1024)\n .describe("Width of the generated image"),\n height: z\n .number()\n .int()\n .min(256)\n .max(2048)\n .optional()\n .default(1024)\n .describe("Height of the generated image"),\n seed: z\n .number()\n .int()\n .optional()\n .default(() => Math.floor(Math.random() * 1000000000000000))\n .describe("Seed for random number generation"),\n steps: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .default(20)\n .describe("Number of sampling steps"),\n cfg_scale: z\n .number()\n .min(0)\n .max(20)\n .optional()\n .default(5.5)\n .describe("Classifier-free guidance scale"),\n sampler_name: z\n .enum(["dpmpp_2m_sde_gpu"])\n .optional()\n .default("dpmpp_2m_sde_gpu")\n .describe("Name of the sampler to use"),\n scheduler: z\n .enum(["exponential"])\n .optional()\n .default("exponential")\n .describe("Type of scheduler to use"),\n denoise: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .default(0.75)\n .describe("Denoising strength"),\n checkpoint,\n image: z.string().describe("Input image for img2img"),\n upscale_method: z\n .enum(["nearest-exact"])\n .optional()\n .default("nearest-exact")\n .describe(\n "Method used for upscaling if input image is smaller than target size"\n ),\n target_width: z\n .number()\n .int()\n .min(256)\n .max(4096)\n .optional()\n .default(1024)\n .describe("Target width for upscaling"),\n target_height: z\n .number()\n .int()\n .min(256)\n .max(4096)\n .optional()\n .default(1024)\n .describe("Target height for upscaling"),\n});\n\ntype InputType = z.infer;\n\nfunction generateWorkflow(input: InputType): Record {\n return {\n "8": {\n inputs: {\n samples: ["36", 0],\n vae: ["14", 2],\n },\n class_type: "VAEDecode",\n _meta: {\n title: "VAE Decode",\n },\n },\n "9": {\n inputs: {\n filename_prefix: "img2img",\n images: ["8", 0],\n },\n class_type: "SaveImage",\n _meta: {\n title: "Save Image",\n },\n },\n "14": {\n inputs: {\n ckpt_name: input.checkpoint,\n },\n class_type: "CheckpointLoaderSimple",\n _meta: {\n title: "Load Checkpoint Base",\n },\n },\n "16": {\n inputs: {\n width: input.width,\n height: input.height,\n crop_w: 0,\n crop_h: 0,\n target_width: input.width,\n target_height: input.height,\n text_g: input.prompt,\n text_l: input.prompt,\n clip: ["14", 1],\n },\n class_type: "CLIPTextEncodeSDXL",\n _meta: {\n title: "CLIPTextEncodeSDXL",\n },\n },\n "19": {\n inputs: {\n width: input.width,\n height: input.height,\n crop_w: 0,\n crop_h: 0,\n target_width: input.width,\n target_height: input.height,\n text_g: input.negative_prompt,\n text_l: input.negative_prompt,\n clip: ["14", 1],\n },\n class_type: "CLIPTextEncodeSDXL",\n _meta: {\n title: "CLIPTextEncodeSDXL",\n },\n },\n "36": {\n inputs: {\n seed: input.seed,\n steps: input.steps,\n cfg: input.cfg_scale,\n sampler_name: input.sampler_name,\n scheduler: input.scheduler,\n denoise: input.denoise,\n model: ["14", 0],\n positive: ["16", 0],\n negative: ["19", 0],\n latent_image: ["39", 0],\n },\n class_type: "KSampler",\n _meta: {\n title: "KSampler",\n },\n },\n "38": {\n inputs: {\n image: input.image,\n upload: "image",\n },\n class_type: "LoadImage",\n _meta: {\n title: "Load Image",\n },\n },\n "39": {\n inputs: {\n pixels: ["40", 0],\n vae: ["14", 2],\n },\n class_type: "VAEEncode",\n _meta: {\n title: "VAE Encode",\n },\n },\n "40": {\n inputs: {\n upscale_method: input.upscale_method,\n width: input.target_width,\n height: input.target_height,\n crop: "center",\n image: ["38", 0],\n },\n class_type: "ImageScale",\n _meta: {\n title: "Upscale Image",\n },\n },\n };\n}\n\nconst workflow: Workflow = {\n RequestSchema,\n generateWorkflow,\n};\n\nexport default workflow;\n\n```\n', + messages: [ + { + role: "user", + content: input, + }, + ], + }, + { + headers: { + "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15", + }, + } + ); + let response = + msg.content[0].type === "text" ? msg.content[0].text : JSON.stringify(msg); + if (response.startsWith("```")) { + const first = response.indexOf("\n"); + response = response.slice(first + 1, response.lastIndexOf("```")); + } + return response; +} + +const usage = `Usage: node generateWorkflow.js `; +async function main() { + // input is the contents of a file provided in the first arg + const inputFile = process.argv[2]; + const outputFile = process.argv[3]; + + assert(inputFile, usage); + assert(outputFile, usage); + + const inputContent = await fs.readFile(inputFile, "utf-8"); + const output = await generateWorkflow(inputContent); + + // Create output directory if it doesn't exist + const outputDir = path.dirname(outputFile); + await fs.mkdir(outputDir, { recursive: true }); + await fs.writeFile(outputFile, output); +} + +main(); diff --git a/package-lock.json b/package-lock.json index 4197463..f111cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "comfyui-wrapper": "dist/src/index.js" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.26.1", "@types/chokidar": "^2.1.3", "@types/node": "^20.12.7", "minimist": "^1.2.8", @@ -27,6 +28,30 @@ "typescript": "^5.4.5" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.26.1.tgz", + "integrity": "sha512-HeMJP1bDFfQPQS3XTJAmfXkFBdZ88wvfkE05+vsoA9zGn5dHqEaHOPsqkazf/i0gXYg2XlLxxZrf6rUAarSqzw==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.44.tgz", + "integrity": "sha512-ZsbGerYg72WMXUIE9fYxtvfzLEuq6q8mKERdWFnqTmOvudMxnz+CBNRoOwJ2kNpFOncrKjT1hZwxjlFgQ9qvQA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@babel/generator": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", @@ -381,6 +406,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -409,6 +444,18 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dev": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", @@ -483,6 +530,12 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -707,6 +760,18 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -785,6 +850,15 @@ "node": ">=4.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1051,6 +1125,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "dev": true + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dev": true, + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1271,6 +1378,15 @@ "node": ">= 6" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1518,6 +1634,27 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -1628,6 +1765,25 @@ "node": ">=10" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2525,6 +2681,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index e44668a..ff7f5f6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "author": "Shawn Rushefsky", "license": "MIT", "devDependencies": { + "@anthropic-ai/sdk": "^0.26.1", "@types/chokidar": "^2.1.3", "@types/node": "^20.12.7", "minimist": "^1.2.8",