├── public ├── logo.webp └── index.html ├── .env.sample ├── package.json ├── .gitignore ├── utils ├── helpers.js ├── ollama.js ├── prompts.js └── anthropic.js ├── workflows ├── sdxl_turbo.json └── flux.json ├── Readme.Md └── app.js /public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahgsql/flux-magic/HEAD/public/logo.webp -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | LLM=OLLAMA # ANTHROPIC OR OLLAMA # ANTHROPIC_API_KEY required when ANTHROPIC |||| OLLAMA_HOST and OLLAMA_MODEL required when OLLAMA 2 | IMAGE=REPLICATE # LOCAL OR REPLICATE # REPLICATE_API_TOKEN and REPLICATE_MODEL required when using REPLICATE |||| COMFY_CLIENT required when using LOCAL 3 | ANTHROPIC_API_KEY= 4 | REPLICATE_API_TOKEN= 5 | REPLICATE_MODEL=flux-schnell # flux-schnell | flux-dev | flux-pro 6 | COMFY_CLIENT=127.0.0.1:8188 7 | OLLAMA_HOST=http://127.0.0.1:11434 8 | OLLAMA_MODEL=aya:8b 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-magic", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "@anthropic-ai/sdk": "^0.25.0", 15 | "ansi-colors": "^4.1.3", 16 | "cli-progress": "^3.12.0", 17 | "comfyui-nodejs": "^1.0.1", 18 | "express": "^4.19.2", 19 | "image-to-base64": "^2.2.0", 20 | "jsonrepair": "^3.8.0", 21 | "ollama": "^0.5.6", 22 | "open": "^10.1.0", 23 | "replicate": "^0.32.0", 24 | "socket.io": "^4.7.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | jspm_packages/ 4 | 5 | # Optional npm cache directory 6 | .npm 7 | 8 | # Optional eslint cache 9 | .eslintcache 10 | 11 | # Optional REPL history 12 | .node_repl_history 13 | 14 | # Output of 'npm pack' 15 | *.tgz 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | 36 | # nyc test coverage 37 | .nyc_output 38 | 39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | bower_components 44 | 45 | # dotenv environment variables file 46 | .env 47 | .env.test 48 | 49 | # parcel-bundler cache (https://parceljs.org/) 50 | .cache 51 | 52 | # Next.js build output 53 | .next 54 | 55 | # Nuxt.js build / generate output 56 | .nuxt 57 | dist 58 | 59 | # Gatsby files 60 | .cache/ 61 | stats.json 62 | 63 | # vuepress build output 64 | .vuepress/dist 65 | 66 | # Serverless directories 67 | .serverless/ 68 | 69 | # FuseBox cache 70 | .fusebox/ 71 | 72 | # DynamoDB Local files 73 | .dynamodb/ 74 | 75 | # OS generated files 76 | .DS_Store 77 | .DS_Store? 78 | ._* 79 | .Spotlight-V100 80 | .Trashes 81 | ehthumbs.db 82 | Thumbs.db 83 | tmp 84 | -------------------------------------------------------------------------------- /utils/helpers.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function findNearestAspectRatio(width, height) { 4 | const aspectRatios = [ 5 | { ratio: "1:1", value: 1 }, 6 | { ratio: "16:9", value: 16 / 9 }, 7 | { ratio: "21:9", value: 21 / 9 }, 8 | { ratio: "2:3", value: 2 / 3 }, 9 | { ratio: "3:2", value: 3 / 2 }, 10 | { ratio: "4:5", value: 4 / 5 }, 11 | { ratio: "5:4", value: 5 / 4 }, 12 | { ratio: "9:16", value: 9 / 16 }, 13 | { ratio: "9:21", value: 9 / 21 } 14 | ]; 15 | 16 | const imageRatio = width / height; 17 | let closestRatio = aspectRatios[0]; 18 | let minDifference = Math.abs(imageRatio - aspectRatios[0].value); 19 | 20 | for (let i = 1; i < aspectRatios.length; i++) { 21 | const difference = Math.abs(imageRatio - aspectRatios[i].value); 22 | if (difference < minDifference) { 23 | minDifference = difference; 24 | closestRatio = aspectRatios[i]; 25 | } 26 | } 27 | 28 | return closestRatio.ratio; 29 | } 30 | export function escapeJsonString(str) { 31 | return str.replace(/[\n\r\t\\\"\'\u2028\u2029]/g, function (char) { 32 | switch (char) { 33 | case '\n': return '\\n'; 34 | case '\r': return '\\r'; 35 | case '\t': return '\\t'; 36 | case '\\': return '\\\\'; 37 | case '"': return '\\"'; 38 | case "'": return "\\'"; 39 | case '\u2028': return '\\u2028'; 40 | case '\u2029': return '\\u2029'; 41 | default: return char; 42 | } 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /utils/ollama.js: -------------------------------------------------------------------------------- 1 | import { Ollama } from 'ollama' 2 | 3 | 4 | import dotenv from 'dotenv'; 5 | import fs from "fs/promises"; 6 | 7 | dotenv.config(); 8 | 9 | /** 10 | * This function uses the Ollama API to interact with the Llama2 model and generate a response based on the input messages. 11 | * 12 | * @param {Array} msgArr - An array containing the system and user messages. 13 | * @param {string} [model="llama2"] - The model to use for generating the response. Defaults to "llama2". 14 | * @returns {Object} - An object containing the generated response content, usage information, and pricing details. 15 | * @throws {Error} - If an error occurs during the API request. 16 | */ 17 | 18 | 19 | async function fileToBase64(filePath) { 20 | try { 21 | const data = await fs.readFile(filePath); 22 | return Buffer.from(data).toString('base64'); 23 | } catch (error) { 24 | console.error('Error reading file:', error); 25 | throw error; 26 | } 27 | } 28 | 29 | export default async function ollamaChat(msgArr) { 30 | console.log("ollama chat started"); 31 | const systemMessage = msgArr.find((msg) => msg.role === "system")?.content || ""; 32 | const userMessages = await Promise.all( 33 | msgArr.filter((msg) => msg.role !== "system").map(async (msg) => { 34 | if (msg.images) { 35 | const base64Images = await Promise.all( 36 | msg.images.map(async (imagePath) => { 37 | try { 38 | return await fileToBase64(imagePath); 39 | } catch (error) { 40 | console.error(`Error converting image ${imagePath} to base64:`, error); 41 | return null; // Handle the error appropriately, maybe skip this image or return a placeholder 42 | } 43 | }) 44 | ); 45 | return { ...msg, images: base64Images.filter(img => img !== null) }; 46 | } else { 47 | return msg; 48 | } 49 | }) 50 | ); 51 | try { 52 | const ollama = new Ollama({ host: process.env.OLLAMA_HOST }) 53 | let model = process.env.OLLAMA_MODEL 54 | const response = await ollama.chat({ 55 | model: model, 56 | messages: [{ role: "system", content: systemMessage }, ...userMessages,], 57 | }); 58 | // The Ollama API does not provide usage information directly, so we'll just return the response content 59 | return { 60 | content: response.message.content, 61 | }; 62 | } catch (error) { 63 | console.error(error.message); 64 | return { 65 | content: "", 66 | usage: null, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /workflows/sdxl_turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "5": { 3 | "inputs": { 4 | "width": {width}, 5 | "height": {height}, 6 | "batch_size": {batchSize} 7 | }, 8 | "class_type": "EmptyLatentImage", 9 | "_meta": { 10 | "title": "Empty Latent Image" 11 | } 12 | }, 13 | "6": { 14 | "inputs": { 15 | "text": "{positive}", 16 | "clip": [ 17 | "20", 18 | 1 19 | ] 20 | }, 21 | "class_type": "CLIPTextEncode", 22 | "_meta": { 23 | "title": "CLIP Text Encode (Prompt)" 24 | } 25 | }, 26 | "7": { 27 | "inputs": { 28 | "text": "{negative}", 29 | "clip": [ 30 | "20", 31 | 1 32 | ] 33 | }, 34 | "class_type": "CLIPTextEncode", 35 | "_meta": { 36 | "title": "CLIP Text Encode (Prompt)" 37 | } 38 | }, 39 | "8": { 40 | "inputs": { 41 | "samples": [ 42 | "13", 43 | 0 44 | ], 45 | "vae": [ 46 | "20", 47 | 2 48 | ] 49 | }, 50 | "class_type": "VAEDecode", 51 | "_meta": { 52 | "title": "VAE Decode" 53 | } 54 | }, 55 | "13": { 56 | "inputs": { 57 | "add_noise": true, 58 | "noise_seed": {seed}, 59 | "cfg": 1, 60 | "model": [ 61 | "20", 62 | 0 63 | ], 64 | "positive": [ 65 | "6", 66 | 0 67 | ], 68 | "negative": [ 69 | "7", 70 | 0 71 | ], 72 | "sampler": [ 73 | "14", 74 | 0 75 | ], 76 | "sigmas": [ 77 | "22", 78 | 0 79 | ], 80 | "latent_image": [ 81 | "5", 82 | 0 83 | ] 84 | }, 85 | "class_type": "SamplerCustom", 86 | "_meta": { 87 | "title": "SamplerCustom" 88 | } 89 | }, 90 | "14": { 91 | "inputs": { 92 | "sampler_name": "euler_ancestral" 93 | }, 94 | "class_type": "KSamplerSelect", 95 | "_meta": { 96 | "title": "KSamplerSelect" 97 | } 98 | }, 99 | "20": { 100 | "inputs": { 101 | "ckpt_name": "Stable-diffusion\\SdXl\\sd_xl_turbo_1.0_fp16.safetensors" 102 | }, 103 | "class_type": "CheckpointLoaderSimple", 104 | "_meta": { 105 | "title": "Load Checkpoint" 106 | } 107 | }, 108 | "22": { 109 | "inputs": { 110 | "steps": {steps}, 111 | "denoise": 1, 112 | "model": [ 113 | "20", 114 | 0 115 | ] 116 | }, 117 | "class_type": "SDTurboScheduler", 118 | "_meta": { 119 | "title": "SDTurboScheduler" 120 | } 121 | }, 122 | "27": { 123 | "inputs": { 124 | "filename_prefix": "ComfyUI", 125 | "images": [ 126 | "8", 127 | 0 128 | ] 129 | }, 130 | "class_type": "SaveImage", 131 | "_meta": { 132 | "title": "Save Image" 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /workflows/flux.json: -------------------------------------------------------------------------------- 1 | { 2 | "5": { 3 | "inputs": { 4 | "width": {width}, 5 | "height": {height}, 6 | "batch_size": {batchSize} 7 | }, 8 | "class_type": "EmptyLatentImage", 9 | "_meta": { 10 | "title": "Empty Latent Image" 11 | } 12 | }, 13 | "6": { 14 | "inputs": { 15 | "text": "{positive}", 16 | "clip": ["11", 0] 17 | }, 18 | "class_type": "CLIPTextEncode", 19 | "_meta": { 20 | "title": "CLIP Text Encode (Prompt)" 21 | } 22 | }, 23 | "8": { 24 | "inputs": { 25 | "samples": ["13", 0], 26 | "vae": ["10", 0] 27 | }, 28 | "class_type": "VAEDecode", 29 | "_meta": { 30 | "title": "VAE Decode" 31 | } 32 | }, 33 | "9": { 34 | "inputs": { 35 | "filename_prefix": "ComfyUI", 36 | "images": ["8", 0] 37 | }, 38 | "class_type": "SaveImage", 39 | "_meta": { 40 | "title": "Save Image" 41 | } 42 | }, 43 | "10": { 44 | "inputs": { 45 | "vae_name": "ae.sft" 46 | }, 47 | "class_type": "VAELoader", 48 | "_meta": { 49 | "title": "Load VAE" 50 | } 51 | }, 52 | "11": { 53 | "inputs": { 54 | "clip_name1": "t5xxl_fp8_e4m3fn.safetensors", 55 | "clip_name2": "clip_l.safetensors", 56 | "type": "flux" 57 | }, 58 | "class_type": "DualCLIPLoader", 59 | "_meta": { 60 | "title": "DualCLIPLoader" 61 | } 62 | }, 63 | "12": { 64 | "inputs": { 65 | "unet_name": "{unet_name}", 66 | "weight_dtype": "fp8_e4m3fn" 67 | }, 68 | "class_type": "UNETLoader", 69 | "_meta": { 70 | "title": "Load Diffusion Model" 71 | } 72 | }, 73 | "13": { 74 | "inputs": { 75 | "noise": ["25", 0], 76 | "guider": ["22", 0], 77 | "sampler": ["16", 0], 78 | "sigmas": ["17", 0], 79 | "latent_image": ["5", 0] 80 | }, 81 | "class_type": "SamplerCustomAdvanced", 82 | "_meta": { 83 | "title": "SamplerCustomAdvanced" 84 | } 85 | }, 86 | "16": { 87 | "inputs": { 88 | "sampler_name": "euler" 89 | }, 90 | "class_type": "KSamplerSelect", 91 | "_meta": { 92 | "title": "KSamplerSelect" 93 | } 94 | }, 95 | "17": { 96 | "inputs": { 97 | "scheduler": "simple", 98 | "steps":{steps}, 99 | "denoise": 1, 100 | "model": ["12", 0] 101 | }, 102 | "class_type": "BasicScheduler", 103 | "_meta": { 104 | "title": "BasicScheduler" 105 | } 106 | }, 107 | "22": { 108 | "inputs": { 109 | "model": ["12", 0], 110 | "conditioning": ["6", 0] 111 | }, 112 | "class_type": "BasicGuider", 113 | "_meta": { 114 | "title": "BasicGuider" 115 | } 116 | }, 117 | "25": { 118 | "inputs": { 119 | "noise_seed": {seed} 120 | }, 121 | "class_type": "RandomNoise", 122 | "_meta": { 123 | "title": "RandomNoise" 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Readme.Md: -------------------------------------------------------------------------------- 1 | # Flux-Magic 2 | 3 | Flux-Magic is an LLM-based image generation software that uses either Anthropic's API or local Ollama for prompt enhancement, and then generates images using either ComfyUI (locally) or Replicate API (online). 4 | 5 | ## Table of Contents 6 | 7 | - [Prerequisites](#prerequisites) 8 | - [Installation](#installation) 9 | - [Configuration](#configuration) 10 | - [Running the Application](#running-the-application) 11 | - [Usage](#usage) 12 | 13 | ## Prerequisites 14 | 15 | Before you begin, ensure you have the following installed: 16 | 17 | - Node.js (v14 or later) 18 | - npm (usually comes with Node.js) 19 | - Git 20 | 21 | If you plan to use local image generation: 22 | 23 | - ComfyUI setup locally (and run it, no need any workflow, its already included) 24 | - Ollama (if using local LLM) 25 | 26 | ## Installation 27 | 28 | 1. Clone the repository: 29 | 30 | ``` 31 | git clone https://github.com/ahgsql/flux-magic.git 32 | cd flux-magic 33 | ``` 34 | 35 | 2. Install the dependencies: 36 | 37 | ``` 38 | npm install 39 | ``` 40 | 41 | ## Configuration 42 | 43 | 1. Create a `.env` file in the root directory of the project. You can also rename .env.sample to .env and fill. 44 | 45 | 2. Add the following configuration to your `.env` file: 46 | 47 | ``` 48 | LLM=OLLAMA # ANTHROPIC or OLLAMA 49 | IMAGE=REPLICATE # LOCAL or REPLICATE 50 | ANTHROPIC_API_KEY=your_anthropic_api_key 51 | REPLICATE_API_TOKEN=your_replicate_api_token 52 | REPLICATE_MODEL=flux-schnell # flux-schnell | flux-dev | flux-pro 53 | COMFY_CLIENT=127.0.0.1:8188 54 | OLLAMA_HOST=http://127.0.0.1:11434 55 | OLLAMA_MODEL=aya:8b 56 | ``` 57 | 58 | 3. Adjust the values according to your setup: 59 | - Set `LLM` to either `ANTHROPIC` or `OLLAMA` 60 | - Set `IMAGE` to either `LOCAL` or `REPLICATE` 61 | - If using Anthropic, provide your `ANTHROPIC_API_KEY` 62 | - If using Replicate, provide your `REPLICATE_API_TOKEN` and choose a `REPLICATE_MODEL` 63 | - If using local ComfyUI, set the correct `COMFY_CLIENT` address 64 | - If using Ollama, set the correct `OLLAMA_HOST` and `OLLAMA_MODEL` 65 | 66 | ## Running the Application 67 | 68 | 1. Start the Express server: 69 | 70 | ``` 71 | node app.js 72 | ``` 73 | 74 | 2. The application should now be running on `http://localhost:3333` . 75 | 76 | ## Usage 77 | 78 | 1. Access the web interface by opening a browser and navigating to `http://localhost:3333`. 79 | 80 | 2. Enter your prompt in the provided text area. 81 | 82 | 3. Select your desired art style and image dimensions. 83 | 84 | 4. Click the "Magic!" button to create your image. 85 | 86 | 5. The generated image will appear in the result area on the right side of the page. 87 | 88 | ## Note 89 | 90 | - If you're using local image generation with ComfyUI, make sure ComfyUI is running before starting Flux-Magic. 91 | - If you're using Ollama for local LLM, ensure Ollama is running and the correct model is loaded. 92 | 93 | For more detailed information about the ComfyUI NodeJS Module, please refer to [the GitHub repository](https://github.com/ahgsql/comfyui-nodejs). 94 | 95 | ## Troubleshooting 96 | 97 | If you encounter any issues: 98 | 99 | 1. Check that all required services (ComfyUI, Ollama) are running if you're using local generation. 100 | 2. Verify that your API keys are correct in the `.env` file. 101 | 3. Check the console output for any error messages. 102 | 103 | If problems persist, please open an issue on the GitHub repository. 104 | -------------------------------------------------------------------------------- /utils/prompts.js: -------------------------------------------------------------------------------- 1 | export function magicPrompt(topic, style,) { 2 | const systemMessage = { 3 | role: "system", 4 | content: "You are an AI assistant specialized in creating detailed prompts for image generation based on given topics and styles. Your task is to analyze the input and create a comprehensive, creative, and coherent prompt that can guide an image generation AI to produce a vivid and accurate representation of the described scene or concept.Your responses are raw json" 5 | }; 6 | 7 | const userMessage = { 8 | role: "user", 9 | content: `Please create a detailed image generation prompt based on the following information: 10 | 11 | Topic: ${topic} 12 | Style: ${style} 13 | 14 | Your prompt should include the following elements : 15 | 16 | 1. Main subject or character description 17 | 2. Background and setting details 18 | 3. Lighting, color scheme, and atmosphere 19 | 4. Any specific actions or poses for characters 20 | 5. Important objects or elements to include 21 | 6. Overall mood or emotion to convey 22 | 23 | dont use \\"Ali\\" this syntax. instead return \"Ali\" (one escape char) 24 | use This format to generating enhancedPrompt string:Main characher or Main subject:... Background:... ... Lighting:... Color scheme:... Atmosphere:... Actions:... Objects:... 25 | Follow these format style, you always say Thing and then colon, then explanation. You can add or remove similar things.Be open minded, creative. 26 | Please ensure your prompt is detailed yet concise, avoiding any explanations or meta-commentary. The prompt should be a single line and JSON value friendly of descriptive text that an image generation AI can interpret directly. 27 | If there is anything related drawing or writing text on something, explain this in every detail, fonts, drawing or writing style, color, pen or brush. You can be creative and humorous about the text content if not provided directly. 28 | Remember to be creative and specific in your descriptions, always return English no matter What language is topic or style. using vivid language to paint a clear mental picture. Avoid vague terms and instead use concrete, descriptive words that clearly communicate the desired visual elements.Be artistic, be aesthetic. Double check if your response is suitable to use in JSON value, put escape charachters if need.Never start with double or single quotes. all your response should be oneline, no multiline and valid JSON value. return me valid JSON with following structure: 29 | {"enhancedPrompt":<>} 30 | ` 31 | }; 32 | 33 | return [systemMessage, userMessage]; 34 | } 35 | export function crazyPrompt(topic, style,) { 36 | const systemMessage = { 37 | role: "system", 38 | content: "You are a crazy AI assistant specialized in creating detailed prompts for image generation based on given topics and styles. Your task is to analyze the input and create a creative, crazy, and unseen prompt that can guide an image generation AI to produce an exaggerated representation of the described scene or concept.Your responses are raw json" 39 | }; 40 | 41 | const userMessage = { 42 | role: "user", 43 | content: `Please create a detailed image generation prompt based on the following information: 44 | 45 | Topic: ${topic} 46 | Style: ${style} 47 | 48 | Your prompt should include the following elements : 49 | 50 | 1. Main subject or character description 51 | 2. Background and setting details 52 | 3. Lighting, color scheme, and atmosphere 53 | 4. Any specific actions or poses for characters 54 | 5. Important objects or elements to include 55 | 6. Overall mood or emotion to convey 56 | 57 | dont use \\"Ali\\" this syntax. instead return \"Ali\" (one escape char) 58 | use This format to generating enhancedPrompt string:Main characher or Main subject:... Background:... ... Lighting:... Color scheme:... Atmosphere:... Actions:... Objects:... 59 | Follow these format style, you always say Thing and then colon, then explanation. You can add or remove similar things.Be open minded, creative. 60 | Please ensure your prompt is detailed yet concise, avoiding any explanations or meta-commentary. The prompt should be a single line and JSON value friendly of descriptive text that an image generation AI can interpret directly. 61 | If there is anything related drawing or writing text on something, explain this in every detail, fonts, drawing or writing style, color, pen or brush. You can be creative and humorous about the text content if not provided directly. 62 | Remember to be open-minded, try new mediums and unseen things and be specific in your descriptions, always return English no matter What language is topic or style.Try different things, surprise me. Add relevant side objects and crazy elements. You can transform a cat into robot or giant village, Try extraordinary transforms and scenes. Expand desired subject and style, act like a cult artist. Be artistic, be aesthetic. Double check if your response is suitable to use in JSON value.Never start with double or single quotes. all your response should be oneline, no multiline and valid JSON value. return me valid JSON with following structure: 63 | {"enhancedPrompt":<>} 64 | ` 65 | }; 66 | 67 | return [systemMessage, userMessage]; 68 | } 69 | -------------------------------------------------------------------------------- /utils/anthropic.js: -------------------------------------------------------------------------------- 1 | import Anthropic from "@anthropic-ai/sdk"; 2 | import dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | 6 | /** 7 | * This function uses the Anthropic API to interact with the Claude-3 model and generate a response based on the input messages. 8 | * 9 | * @param {Array} msgArr - An array containing the system and user messages. 10 | * @param {string} [model="haiku"] - The model to use for generating the response. Defaults to "haiku". 11 | * @returns {Object} - An object containing the generated response content, usage information, and pricing details. 12 | * @throws {Error} - If an error occurs during the API request. 13 | */ 14 | export default async function anthropicChat(msgArr, model = "haiku") { 15 | const anthropic = new Anthropic({ 16 | apiKey: process.env.ANTHROPIC_API_KEY, 17 | }); 18 | 19 | const latestModels = [ 20 | "claude-3-opus-20240229", 21 | "claude-3-5-sonnet-20240620", 22 | "claude-3-haiku-20240307", 23 | ]; 24 | 25 | const modelPricing = [ 26 | { 27 | model: "claude-3-opus-20240229", 28 | mTokInput: 15, 29 | mTokOutput: 75, 30 | }, 31 | { 32 | model: "claude-3-5-sonnet-20240620", 33 | mTokInput: 3, 34 | mTokOutput: 15, 35 | }, 36 | { 37 | model: "claude-3-haiku-20240307", 38 | mTokInput: 0.25, 39 | mTokOutput: 1.25, 40 | }, 41 | ]; 42 | 43 | const selectedModel = 44 | latestModels.find((m) => m.includes(model)) || "claude-3-haiku-20240307"; 45 | 46 | const systemMessage = 47 | msgArr.find((msg) => msg.role === "system")?.content || ""; 48 | const userMessages = msgArr.filter((msg) => msg.role !== "system"); 49 | 50 | let selectedPricing = modelPricing.find((m) => m.model === selectedModel); 51 | 52 | try { 53 | const response = await anthropic.messages.create({ 54 | system: systemMessage, 55 | max_tokens: 4096, 56 | messages: [...userMessages], 57 | model: selectedModel, 58 | stop_sequences: ["end_turn"], 59 | }); 60 | 61 | return { 62 | content: response.content[0].text, 63 | usage: response.usage, 64 | usageInCents: { 65 | input: response.usage.input_tokens * selectedPricing.mTokInput / 1000000, 66 | output: response.usage.output_tokens * selectedPricing.mTokOutput / 1000000, 67 | total: parseFloat( 68 | ((response.usage.input_tokens * selectedPricing.mTokInput / 1000000) + 69 | (response.usage.output_tokens * selectedPricing.mTokOutput / 1000000)).toFixed(6) 70 | ), 71 | }, 72 | }; 73 | } catch (error) { 74 | console.error(error.message); 75 | return { 76 | content: "", 77 | usage: null, 78 | }; 79 | } 80 | } 81 | 82 | 83 | 84 | /** 85 | * This function uses the Anthropic API to interact with the Claude-3 model and generate a streamed response based on the input messages. 86 | * 87 | * @param {Array} messages - An array of message objects. 88 | * @param {string} [model="claude-3-haiku-20240307"] - The model to use for generating the response. 89 | * @param {function} streamCallback - A callback function to handle each chunk of the streamed response. 90 | * @returns {Promise} - A promise that resolves to an object containing usage information and pricing details. 91 | * @throws {Error} - If an error occurs during the API request. 92 | */ 93 | export async function anthropicStreamChat(msgArr, model = "haiku", streamCallback) { 94 | const anthropic = new Anthropic({ 95 | apiKey: process.env.ANTHROPIC_API_KEY, 96 | }); 97 | 98 | const latestModels = [ 99 | "claude-3-opus-20240229", 100 | "claude-3-5-sonnet-20240620", 101 | "claude-3-haiku-20240307", 102 | ]; 103 | 104 | const modelPricing = [ 105 | { 106 | model: "claude-3-opus-20240229", 107 | mTokInput: 15, 108 | mTokOutput: 75, 109 | }, 110 | { 111 | model: "claude-3-5-sonnet-20240620", 112 | mTokInput: 3, 113 | mTokOutput: 15, 114 | }, 115 | { 116 | model: "claude-3-haiku-20240307", 117 | mTokInput: 0.25, 118 | mTokOutput: 1.25, 119 | }, 120 | ]; 121 | 122 | const selectedModel = 123 | latestModels.find((m) => m.includes(model)) || "claude-3-haiku-20240307"; 124 | 125 | const systemMessage = 126 | msgArr.find((msg) => msg.role === "system")?.content || ""; 127 | const userMessages = msgArr.filter((msg) => msg.role !== "system"); 128 | 129 | let selectedPricing = modelPricing.find((m) => m.model === selectedModel); 130 | 131 | try { 132 | const stream = await anthropic.messages.create({ 133 | system: systemMessage, 134 | max_tokens: 1500, 135 | messages: [...userMessages], 136 | model: selectedModel, 137 | stream: true, 138 | }); 139 | 140 | let fullContent = ""; 141 | for await (const chunk of stream) { 142 | if (chunk.type === 'content_block_delta') { 143 | fullContent += chunk.delta.text; 144 | streamCallback(chunk.delta.text); 145 | } 146 | } 147 | 148 | // Estimate token usage based on the full content 149 | 150 | return { 151 | finalMessage: fullContent 152 | }; 153 | } catch (error) { 154 | console.error(error.message); 155 | throw error; 156 | } 157 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { createServer } from 'http'; 3 | import { Server } from 'socket.io'; 4 | import { startComfyUi, initClient, randomSeed } from "comfyui-nodejs"; 5 | import Workflowloader from "comfyui-nodejs"; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | import anthropicChat from './utils/anthropic.js'; 9 | import { magicPrompt, crazyPrompt } from './utils/prompts.js'; 10 | import open from 'open'; 11 | import Replicate from "replicate"; 12 | import imageToBase64 from 'image-to-base64'; 13 | import dotenv from "dotenv"; 14 | import ollamaChat from './utils/ollama.js'; 15 | import { findNearestAspectRatio } from './utils/helpers.js'; 16 | import fs from 'fs/promises'; 17 | import { escapeJsonString } from './utils/helpers.js'; // Bu satırı dosyanın başına ekleyin 18 | 19 | dotenv.config(); 20 | 21 | const __filename = fileURLToPath(import.meta.url); 22 | const __dirname = path.dirname(__filename); 23 | 24 | const replicate = new Replicate(); 25 | const app = express(); 26 | const httpServer = createServer(app); 27 | const io = new Server(httpServer); 28 | 29 | const port = 3333; 30 | 31 | app.use(express.json()); 32 | app.use(express.static('public')); 33 | 34 | let client; 35 | let flux; 36 | 37 | const statsFile = path.join(__dirname, 'stats.json'); 38 | 39 | 40 | async function loadStats() { 41 | try { 42 | const data = await fs.readFile(statsFile, 'utf8'); 43 | return JSON.parse(data); 44 | } catch (error) { 45 | if (error.code === 'ENOENT') { 46 | const initialStats = { 47 | totalImagesGenerated: 0, 48 | models: {} 49 | }; 50 | await fs.writeFile(statsFile, JSON.stringify(initialStats, null, 2)); 51 | return initialStats; 52 | } 53 | throw error; 54 | } 55 | } 56 | 57 | async function updateStats(width, height, generationTime, llmTime, model) { 58 | const stats = await loadStats(); 59 | const resolution = `${width}x${height}`; 60 | 61 | if (!stats.models[model]) { 62 | stats.models[model] = {}; 63 | } 64 | 65 | if (!stats.models[model][resolution]) { 66 | stats.models[model][resolution] = { 67 | totalTime: 0, 68 | count: 0, 69 | llmTime: 0 70 | }; 71 | } 72 | 73 | stats.models[model][resolution].totalTime += generationTime; 74 | stats.models[model][resolution].llmTime += llmTime; 75 | stats.models[model][resolution].count += 1; 76 | stats.totalImagesGenerated += 1; 77 | 78 | await fs.writeFile(statsFile, JSON.stringify(stats, null, 2)); 79 | } 80 | 81 | (async () => { 82 | client = await initClient(process.env.COMFY_CLIENT); 83 | await client.connect(); 84 | flux = new Workflowloader("flux.json", client, true); 85 | const initialStats = await loadStats(); 86 | console.log("Initial stats loaded:", initialStats); 87 | })(); 88 | 89 | app.get('/', (req, res) => { 90 | res.sendFile(path.join(__dirname, 'public', 'index.html')); 91 | }); 92 | 93 | app.post('/generate', async (req, res) => { 94 | try { 95 | const stats = await loadStats(); // Her istek için en güncel istatistikleri yükle 96 | const { prompt, width, height, style, skipLLM, crazyLLM, model = "dev" } = req.body; 97 | let finalPrompt; 98 | let llmStartTime, llmEndTime, imageStartTime, imageEndTime; 99 | 100 | llmStartTime = Date.now(); 101 | if (skipLLM) { 102 | finalPrompt = escapeJsonString(style + " style " + prompt + " in the style of " + style); 103 | llmEndTime = llmStartTime; 104 | } else { 105 | let aiPrompt 106 | if (crazyLLM) { 107 | aiPrompt = crazyPrompt(prompt, style); 108 | } else { 109 | aiPrompt = magicPrompt(prompt, style); 110 | 111 | } 112 | if (process.env.LLM == "ANTHROPIC") { 113 | const response = await anthropicChat(aiPrompt, "sonnet"); 114 | console.log(response); 115 | finalPrompt = JSON.parse(response.content).enhancedPrompt; 116 | } else { 117 | const response = await ollamaChat(aiPrompt); 118 | finalPrompt = JSON.parse(response.content).enhancedPrompt; 119 | } 120 | llmEndTime = Date.now(); 121 | } 122 | 123 | const llmTime = llmEndTime - llmStartTime; 124 | 125 | const resolution = `${width}x${height}`; 126 | const modelStats = stats.models[model] || {}; 127 | const resolutionStats = modelStats[resolution] || { totalTime: 0, count: 0, llmTime: 0 }; 128 | const avgGenerationTime = resolutionStats.count > 0 129 | ? (resolutionStats.totalTime + resolutionStats.llmTime) / resolutionStats.count 130 | : 30000; // Default to 30 seconds if no data 131 | 132 | imageStartTime = Date.now(); 133 | 134 | imageStartTime = Date.now(); 135 | 136 | if (process.env.IMAGE == "LOCAL") { 137 | flux.prepare({ 138 | positive: finalPrompt, 139 | steps: (model == "dev" ? 25 : 5), 140 | batchSize: 1, 141 | seed: 514, 142 | width: parseInt(width), 143 | height: parseInt(height), 144 | unet_name: "flux1-" + model + ".sft" 145 | }); 146 | 147 | const estimatedTime = Math.round(avgGenerationTime / 1000); 148 | const updateInterval = setInterval(() => { 149 | const elapsedSeconds = Math.round((Date.now() - imageStartTime) / 1000); 150 | const progress = Math.min(elapsedSeconds / estimatedTime, 1); 151 | io.emit('progressUpdate', { progress: progress * 100 }); 152 | }, 1000); 153 | 154 | const result = await flux.generate(); 155 | 156 | clearInterval(updateInterval); 157 | 158 | imageEndTime = Date.now(); 159 | const actualTime = imageEndTime - imageStartTime; 160 | await updateStats(width, height, actualTime, llmTime, model); 161 | 162 | 163 | console.log(`\nActual generation time: ${actualTime} ms`); 164 | io.emit('progressUpdate', { progress: 100 }); 165 | res.json({ success: true, enhancedPrompt: finalPrompt, images: result.map(img => ({ base64: img.base64.split(',')[1] })) }); 166 | } else { 167 | let ar = findNearestAspectRatio(parseInt(width), parseInt(height)); 168 | const input = { 169 | prompt: finalPrompt, 170 | output_quality: 100, 171 | aspect_ratio: ar, 172 | }; 173 | 174 | const output = await replicate.run("black-forest-labs/" + process.env.REPLICATE_MODEL, { input }); 175 | let base64 = await imageToBase64(output); 176 | res.json({ success: true, enhancedPrompt: finalPrompt, images: base64 }); 177 | } 178 | } catch (error) { 179 | io.emit('progressUpdate', { progress: 100 }); 180 | console.error(error); 181 | res.status(500).json({ success: false, error: error.message }); 182 | } 183 | }); 184 | 185 | io.on('connection', (socket) => { 186 | console.log('A user connected'); 187 | socket.on('disconnect', () => { 188 | console.log('User disconnected'); 189 | }); 190 | }); 191 | 192 | httpServer.listen(port, async () => { 193 | console.log(`Server running at http://localhost:${port}`); 194 | await open("http://localhost:" + port); 195 | }); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ComfyUI Web Interface 7 | 11 | 114 | 118 | 119 | 120 | 121 |
122 |
123 |
127 | 128 |
129 | 135 | 141 |
142 |
143 | 149 | 181 | 188 |
189 |
190 | 196 |
197 | 201 |
202 |
203 |
204 | 208 |
209 |
210 | 214 |
215 |
216 | Flux Model 217 | 218 |
219 |
220 | 224 |
225 |
226 | 236 |
237 |
238 | 239 |
240 | 247 |
248 | 258 |
259 |
260 |
261 |
262 | 263 | 272 |
273 |

274 | Powered by the magical, occasionally caffeinated electrons of 275 | 281 | Flux-Magic 282 | 283 | . Warning: May cause spontaneous bouts of creativity and uncontrollable 284 | urges to redecorate. 285 |

286 |
287 | 288 | 289 | 460 | 461 | 462 | --------------------------------------------------------------------------------