@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
170 lines (158 loc) • 5.55 kB
text/typescript
import { generateUniqueSeeds } from '@lobechat/utils';
import { PromptBuilder } from '@saintno/comfyui-sdk';
import { WORKFLOW_DEFAULTS } from '@/server/services/comfyui/config/constants';
import type { WorkflowContext } from '@/server/services/comfyui/core/workflowBuilderService';
import { splitPromptForDualCLIP } from '@/server/services/comfyui/utils/promptSplitter';
import { selectOptimalWeightDtype } from '@/server/services/comfyui/utils/weightDType';
import { getWorkflowFilenamePrefix } from '@/server/services/comfyui/utils/workflowUtils';
/**
* FLUX Schnell Workflow Builder
*
* @description Builds 4-step fast generation workflow optimized for speed
*
* @param {string} modelFileName - Model filename
* @param {Record<string, any>} params - Generation parameters
* @param {WorkflowContext} context - Workflow context
* @returns {PromptBuilder<any, any, any>} Built workflow
*/
export async function buildFluxSchnellWorkflow(
modelFileName: string,
params: Record<string, any>,
context: WorkflowContext,
): Promise<PromptBuilder<any, any, any>> {
// Get required components - will throw if not available (workflow cannot run without them)
const selectedT5Model = await context.modelResolverService.getOptimalComponent('t5', 'FLUX');
const selectedVAE = await context.modelResolverService.getOptimalComponent('vae', 'FLUX');
const selectedCLIP = await context.modelResolverService.getOptimalComponent('clip', 'FLUX');
// Process prompt splitting early in workflow construction
const { t5xxlPrompt, clipLPrompt } = splitPromptForDualCLIP(params.prompt);
/* eslint-disable sort-keys-fix/sort-keys-fix */
const workflow = {
'1': {
_meta: {
title: 'DualCLIP Loader',
},
class_type: 'DualCLIPLoader',
inputs: {
clip_name1: selectedT5Model,
clip_name2: selectedCLIP,
type: 'flux',
},
},
'2': {
_meta: {
title: 'UNET Loader',
},
class_type: 'UNETLoader',
inputs: {
unet_name: modelFileName,
weight_dtype: selectOptimalWeightDtype(modelFileName),
},
},
'3': {
_meta: {
title: 'VAE Loader',
},
class_type: 'VAELoader',
inputs: {
vae_name: selectedVAE,
},
},
'4': {
_meta: {
title: 'CLIP Text Encode (Flux)',
},
class_type: 'CLIPTextEncodeFlux',
inputs: {
clip: ['1', 0],
clip_l: clipLPrompt,
guidance: 1,
t5xxl: t5xxlPrompt, // Schnell uses CFG 1
},
},
'5': {
_meta: {
title: 'Empty SD3 Latent Image',
},
class_type: 'EmptySD3LatentImage',
inputs: {
batch_size: WORKFLOW_DEFAULTS.IMAGE.BATCH_SIZE,
height: params.height,
width: params.width,
},
},
'6': {
_meta: {
title: 'K Sampler',
},
class_type: 'KSampler',
inputs: {
cfg: 1,
denoise: WORKFLOW_DEFAULTS.SAMPLING.DENOISE,
latent_image: ['5', 0],
model: ['2', 0],
negative: ['4', 0],
positive: ['4', 0],
sampler_name: params.samplerName,
scheduler: params.scheduler,
seed: params.seed ?? generateUniqueSeeds(1)[0],
steps: params.steps,
},
},
'7': {
_meta: {
title: 'VAE Decode',
},
class_type: 'VAEDecode',
inputs: {
samples: ['6', 0],
vae: ['3', 0],
},
},
'8': {
_meta: {
title: 'Save Image',
},
class_type: 'SaveImage',
inputs: {
filename_prefix: getWorkflowFilenamePrefix('buildFluxSchnellWorkflow', context.variant),
images: ['7', 0],
},
},
};
/* eslint-enable sort-keys-fix/sort-keys-fix */
// Set prompt values directly to workflow nodes instead of using PromptBuilder input mapping
workflow['4'].inputs.clip_l = clipLPrompt;
workflow['4'].inputs.t5xxl = t5xxlPrompt;
// Set shared values directly to avoid conflicts - use params directly without intermediate variables
workflow['5'].inputs.width = params.width; // EmptySD3LatentImage needs width/height
workflow['5'].inputs.height = params.height;
workflow['4'].inputs.guidance = params.cfg; // CLIPTextEncodeFlux needs guidance
workflow['6'].inputs.cfg = params.cfg; // KSampler needs cfg
workflow['6'].inputs.steps = params.steps; // KSampler needs steps
workflow['6'].inputs.seed = params.seed ?? generateUniqueSeeds(1)[0]; // KSampler needs seed
workflow['6'].inputs.scheduler = params.scheduler; // KSampler needs scheduler
workflow['6'].inputs.sampler_name = params.samplerName; // KSampler needs sampler_name
// Create PromptBuilder - removed prompt input parameters as they are set directly
const builder = new PromptBuilder(
workflow,
['width', 'height', 'steps', 'cfg', 'seed', 'scheduler', 'sampler_name'],
['images'],
);
// Set output node
builder.setOutputNode('images', '8');
// Set input node mappings
builder.setInputNode('seed', '6.inputs.seed');
builder.setInputNode('width', '5.inputs.width');
builder.setInputNode('height', '5.inputs.height');
builder.setInputNode('steps', '6.inputs.steps');
builder.setInputNode('cfg', '6.inputs.cfg');
// Set input values (prompt already set directly in workflow)
builder
.input('width', params.width)
.input('height', params.height)
.input('steps', params.steps)
.input('cfg', params.cfg)
.input('seed', params.seed ?? generateUniqueSeeds(1)[0]);
return builder;
}