mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
Merge f9d119d127 into 56d296ef1b
This commit is contained in:
commit
20fa2be202
4 changed files with 523 additions and 233 deletions
|
|
@ -393,7 +393,10 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
|
|||
model_node_id = None
|
||||
|
||||
for node in image_config.COMFYUI_WORKFLOW_NODES:
|
||||
if node['type'] == 'model':
|
||||
node_type = node.get('type', '')
|
||||
node_key = node.get('key', '')
|
||||
is_model_node = node_type == 'model' or node_key.endswith('_name')
|
||||
if is_model_node:
|
||||
if node['node_ids']:
|
||||
model_node_id = node['node_ids'][0]
|
||||
break
|
||||
|
|
@ -450,6 +453,8 @@ class CreateImageForm(BaseModel):
|
|||
n: int = 1
|
||||
steps: int | None = None
|
||||
negative_prompt: str | None = None
|
||||
seed: int | None = None
|
||||
extra_params: dict | None = None
|
||||
|
||||
|
||||
GenerateImageForm = CreateImageForm # Alias for backward compatibility
|
||||
|
|
@ -744,6 +749,12 @@ async def image_generations(
|
|||
if form_data.negative_prompt is not None:
|
||||
data['negative_prompt'] = form_data.negative_prompt
|
||||
|
||||
if form_data.seed is not None:
|
||||
data['seed'] = form_data.seed
|
||||
|
||||
if form_data.extra_params:
|
||||
data['extra_params'] = form_data.extra_params
|
||||
|
||||
form_data = ComfyUICreateImageForm(
|
||||
**{
|
||||
'workflow': ComfyUIWorkflow(
|
||||
|
|
@ -764,6 +775,15 @@ async def image_generations(
|
|||
)
|
||||
log.debug('res: %s', res)
|
||||
|
||||
if res is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.DEFAULT(
|
||||
'ComfyUI image generation failed. Check that ComfyUI is running, '
|
||||
'the Base URL is correct, and the workflow is valid.'
|
||||
),
|
||||
)
|
||||
|
||||
images = []
|
||||
|
||||
for image in res['data']:
|
||||
|
|
@ -833,6 +853,7 @@ async def image_generations(
|
|||
images.append({'url': url})
|
||||
return images
|
||||
except Exception as e:
|
||||
log.exception(f'[image_generations] Unhandled exception: {e}')
|
||||
error = e
|
||||
if isinstance(e, aiohttp.ClientResponseError):
|
||||
error = e.message
|
||||
|
|
@ -847,6 +868,9 @@ class EditImageForm(BaseModel):
|
|||
n: int | None = None
|
||||
negative_prompt: str | None = None
|
||||
background: str | None = None
|
||||
seed: int | None = None
|
||||
steps: int | None = None
|
||||
extra_params: dict | None = None
|
||||
|
||||
|
||||
@router.post('/edit')
|
||||
|
|
@ -1130,6 +1154,18 @@ async def image_edits(
|
|||
**({'n': form_data.n} if form_data.n else {}),
|
||||
}
|
||||
|
||||
if form_data.negative_prompt is not None:
|
||||
data['negative_prompt'] = form_data.negative_prompt
|
||||
|
||||
if form_data.seed is not None:
|
||||
data['seed'] = form_data.seed
|
||||
|
||||
if form_data.steps is not None:
|
||||
data['steps'] = form_data.steps
|
||||
|
||||
if form_data.extra_params:
|
||||
data['extra_params'] = form_data.extra_params
|
||||
|
||||
form_data = ComfyUIEditImageForm(
|
||||
**{
|
||||
'workflow': ComfyUIWorkflow(
|
||||
|
|
@ -1150,6 +1186,15 @@ async def image_edits(
|
|||
)
|
||||
log.debug('res: %s', res)
|
||||
|
||||
if res is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.DEFAULT(
|
||||
'ComfyUI image edit failed. Check that ComfyUI is running, '
|
||||
'the Base URL is correct, and the workflow is valid.'
|
||||
),
|
||||
)
|
||||
|
||||
image_urls = set()
|
||||
for image in res['data']:
|
||||
image_urls.add(image['url'])
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async def queue_prompt(prompt, client_id, base_url, api_key):
|
|||
try:
|
||||
session = await get_session()
|
||||
async with session.post(
|
||||
f'{base_url}/prompt',
|
||||
f'{base_url}/api/prompt',
|
||||
json=p,
|
||||
headers={**default_headers, 'Authorization': f'Bearer {api_key}'},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
|
|
@ -71,7 +71,12 @@ async def _ws_get_images(ws, workflow, client_id, base_url, api_key):
|
|||
|
||||
Returns a dict of ``{'data': [{'url': ...}, ...]}``.
|
||||
"""
|
||||
prompt_id = (await queue_prompt(workflow, client_id, base_url, api_key))['prompt_id']
|
||||
queue_response = await queue_prompt(workflow, client_id, base_url, api_key)
|
||||
if not queue_response or 'prompt_id' not in queue_response:
|
||||
log.error(f'ComfyUI queue_prompt returned unexpected response: {queue_response}')
|
||||
raise RuntimeError(f'ComfyUI did not return a prompt_id. Response: {queue_response}')
|
||||
|
||||
prompt_id = queue_response['prompt_id']
|
||||
output_images = []
|
||||
|
||||
async for msg in ws:
|
||||
|
|
@ -86,8 +91,13 @@ async def _ws_get_images(ws, workflow, client_id, base_url, api_key):
|
|||
break
|
||||
# binary messages (previews) are silently skipped
|
||||
|
||||
history = (await get_history(prompt_id, base_url, api_key))[prompt_id]
|
||||
for node_id in history['outputs']:
|
||||
history_map = await get_history(prompt_id, base_url, api_key)
|
||||
history = history_map.get(prompt_id) if history_map else None
|
||||
if history is None:
|
||||
log.error(f'ComfyUI history missing for prompt_id={prompt_id}. Full response: {history_map}')
|
||||
return {'data': []}
|
||||
|
||||
for node_id in history.get('outputs', {}):
|
||||
node_output = history['outputs'][node_id]
|
||||
if node_id in workflow and workflow[node_id].get('class_type') in [
|
||||
'SaveImage',
|
||||
|
|
@ -142,48 +152,95 @@ class ComfyUICreateImageForm(BaseModel):
|
|||
|
||||
steps: Optional[int] = None
|
||||
seed: Optional[int] = None
|
||||
extra_params: Optional[dict] = None
|
||||
|
||||
|
||||
def _apply_workflow_nodes(workflow, nodes, model, payload):
|
||||
"""Mutate *workflow* dict in-place based on typed node definitions."""
|
||||
"""Mutate *workflow* dict in-place based on typed node definitions.
|
||||
|
||||
Supports both the legacy hardcoded type strings (e.g. 'prompt', 'model',
|
||||
'width', 'height', 'steps', 'seed', 'image', 'n') and the new dynamic
|
||||
format produced by the auto-detection feature ('ClassName::inputKey').
|
||||
|
||||
For the dynamic format, injection is decided by the *key* field (the
|
||||
ComfyUI input name), falling back to the ``node.value`` passthrough for
|
||||
any key that does not map to a known semantic slot.
|
||||
"""
|
||||
for node in nodes:
|
||||
if node.type:
|
||||
if node.type == 'model':
|
||||
node_type = node.type or ''
|
||||
node_key = node.key or ''
|
||||
|
||||
if node_type == 'model':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key] = model
|
||||
elif node_type == 'prompt':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key if node_key else 'text'] = payload.prompt
|
||||
elif node_type == 'negative_prompt':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key if node_key else 'text'] = payload.negative_prompt
|
||||
elif node_type == 'image':
|
||||
if isinstance(payload.image, list):
|
||||
for idx, node_id in enumerate(node.node_ids):
|
||||
if idx < len(payload.image):
|
||||
workflow[node_id]['inputs'][node_key] = payload.image[idx]
|
||||
else:
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key] = model
|
||||
elif node.type == 'prompt':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key if node.key else 'text'] = payload.prompt
|
||||
elif node.type == 'negative_prompt':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key if node.key else 'text'] = payload.negative_prompt
|
||||
elif node.type == 'image':
|
||||
if isinstance(payload.image, list):
|
||||
for idx, node_id in enumerate(node.node_ids):
|
||||
if idx < len(payload.image):
|
||||
workflow[node_id]['inputs'][node.key] = payload.image[idx]
|
||||
else:
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key] = payload.image
|
||||
elif node.type == 'width':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key if node.key else 'width'] = payload.width
|
||||
elif node.type == 'height':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key if node.key else 'height'] = payload.height
|
||||
elif node.type == 'n':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key if node.key else 'batch_size'] = payload.n
|
||||
elif node.type == 'steps':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key if node.key else 'steps'] = payload.steps
|
||||
elif node.type == 'seed':
|
||||
seed = payload.seed if payload.seed else random.randint(0, 1125899906842624)
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key] = seed
|
||||
workflow[node_id]['inputs'][node_key] = payload.image
|
||||
elif node_type == 'width':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key if node_key else 'width'] = payload.width
|
||||
elif node_type == 'height':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key if node_key else 'height'] = payload.height
|
||||
elif node_type == 'n':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key if node_key else 'batch_size'] = payload.n
|
||||
elif node_type == 'steps':
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key if node_key else 'steps'] = payload.steps
|
||||
elif node_type == 'seed':
|
||||
seed = payload.seed if payload.seed else random.randint(0, 1125899906842624)
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node_key] = seed
|
||||
|
||||
elif '::' in node_type:
|
||||
for node_id in node.node_ids:
|
||||
if node_id not in workflow:
|
||||
continue
|
||||
if node_key in ('ckpt_name', 'unet_name'):
|
||||
workflow[node_id]['inputs'][node_key] = model
|
||||
elif node_key in ('text', 'prompt', 'positive'):
|
||||
workflow[node_id]['inputs'][node_key] = payload.prompt
|
||||
elif node_key in ('width',):
|
||||
workflow[node_id]['inputs'][node_key] = payload.width
|
||||
elif node_key in ('height',):
|
||||
workflow[node_id]['inputs'][node_key] = payload.height
|
||||
elif node_key in ('steps',):
|
||||
if payload.steps is not None:
|
||||
workflow[node_id]['inputs'][node_key] = payload.steps
|
||||
elif node_key in ('seed', 'noise_seed'):
|
||||
seed = payload.seed if payload.seed else random.randint(0, 1125899906842624)
|
||||
workflow[node_id]['inputs'][node_key] = seed
|
||||
elif node_key in ('batch_size',):
|
||||
workflow[node_id]['inputs'][node_key] = payload.n
|
||||
elif node_key in ('image',):
|
||||
if hasattr(payload, 'image'):
|
||||
img = payload.image
|
||||
if isinstance(img, list):
|
||||
idx = node.node_ids.index(node_id)
|
||||
if idx < len(img):
|
||||
workflow[node_id]['inputs'][node_key] = img[idx]
|
||||
else:
|
||||
workflow[node_id]['inputs'][node_key] = img
|
||||
elif node.value is not None:
|
||||
workflow[node_id]['inputs'][node_key] = node.value
|
||||
elif hasattr(payload, 'extra_params') and payload.extra_params and node_key in payload.extra_params:
|
||||
workflow[node_id]['inputs'][node_key] = payload.extra_params[node_key]
|
||||
|
||||
else:
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]['inputs'][node.key] = node.value
|
||||
workflow[node_id]['inputs'][node_key] = node.value
|
||||
|
||||
|
||||
async def comfyui_create_image(model: str, payload: ComfyUICreateImageForm, client_id, base_url, api_key):
|
||||
|
|
@ -219,12 +276,14 @@ class ComfyUIEditImageForm(BaseModel):
|
|||
|
||||
image: str | list[str]
|
||||
prompt: str
|
||||
negative_prompt: Optional[str] = None
|
||||
width: Optional[int] = None
|
||||
height: Optional[int] = None
|
||||
n: Optional[int] = None
|
||||
|
||||
steps: Optional[int] = None
|
||||
seed: Optional[int] = None
|
||||
extra_params: Optional[dict] = None
|
||||
|
||||
|
||||
async def comfyui_edit_image(model: str, payload: ComfyUIEditImageForm, client_id, base_url, api_key):
|
||||
|
|
|
|||
|
|
@ -38,67 +38,227 @@
|
|||
'w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
|
||||
let showComfyUIWorkflowEditor = false;
|
||||
let REQUIRED_WORKFLOW_NODES = [
|
||||
{
|
||||
type: 'prompt',
|
||||
key: 'text',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'model',
|
||||
key: 'ckpt_name',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'width',
|
||||
key: 'width',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'height',
|
||||
key: 'height',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'steps',
|
||||
key: 'steps',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'seed',
|
||||
key: 'seed',
|
||||
node_ids: ''
|
||||
|
||||
let workflowNodesConfig: { type: string; key: string; node_ids: string; class_type: string }[] =
|
||||
[];
|
||||
let lastKnownWorkflowString: string | null = null;
|
||||
|
||||
function parseAndPopulateWorkflowNodes(
|
||||
workflow: Record<string, any>,
|
||||
savedNodes: { type: string; key: string; node_ids: string[] | string }[] = [],
|
||||
showToast = false
|
||||
): boolean {
|
||||
if (!workflow || typeof workflow !== 'object') {
|
||||
if (showToast) toast.error($i18n.t('Invalid workflow data provided for parsing.'));
|
||||
workflowNodesConfig = [];
|
||||
return false;
|
||||
}
|
||||
];
|
||||
|
||||
const nodeGroups = new Map<
|
||||
string,
|
||||
{ type: string; key: string; node_ids: string[]; class_type: string }
|
||||
>();
|
||||
let discoveredPrimitiveCount = 0;
|
||||
|
||||
try {
|
||||
for (const nodeId of Object.keys(workflow)) {
|
||||
const node = workflow[nodeId];
|
||||
if (!node || typeof node !== 'object' || !node.inputs || typeof node.inputs !== 'object')
|
||||
continue;
|
||||
|
||||
for (const inputKey of Object.keys(node.inputs)) {
|
||||
const val = node.inputs[inputKey];
|
||||
const valType = typeof val;
|
||||
|
||||
if (valType !== 'string' && valType !== 'number' && valType !== 'boolean') continue;
|
||||
|
||||
discoveredPrimitiveCount++;
|
||||
|
||||
const entryKey = `${nodeId}::${node.class_type}::${inputKey}`;
|
||||
const semanticType = `${node.class_type}::${inputKey}`;
|
||||
|
||||
const saved = savedNodes.find(
|
||||
(s) =>
|
||||
s.type === semanticType &&
|
||||
s.key === inputKey &&
|
||||
Array.isArray(s.node_ids) &&
|
||||
s.node_ids.length > 0 &&
|
||||
(s.node_ids as string[]).includes(nodeId)
|
||||
);
|
||||
|
||||
if (!nodeGroups.has(entryKey)) {
|
||||
const ambiguousKeys = new Set(['text', 'prompt', 'positive']);
|
||||
const autoAssign = !ambiguousKeys.has(inputKey);
|
||||
|
||||
nodeGroups.set(entryKey, {
|
||||
type: semanticType,
|
||||
key: inputKey,
|
||||
node_ids: autoAssign ? [nodeId] : [],
|
||||
class_type: node.class_type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing workflow nodes:', err);
|
||||
if (showToast) toast.error($i18n.t('Error occurred during workflow parsing.'));
|
||||
workflowNodesConfig = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
workflowNodesConfig = Array.from(nodeGroups.values()).map((n) => ({
|
||||
...n,
|
||||
node_ids: n.node_ids.join(',')
|
||||
}));
|
||||
|
||||
if (showToast) {
|
||||
if (workflowNodesConfig.length > 0) {
|
||||
toast.success(
|
||||
$i18n.t(
|
||||
`Workflow parsed. {{count}} configurable input(s) found. Please review the Node IDs.`,
|
||||
{ count: workflowNodesConfig.length }
|
||||
)
|
||||
);
|
||||
} else if (discoveredPrimitiveCount === 0 && Object.keys(workflow).length > 0) {
|
||||
toast.info(
|
||||
$i18n.t(
|
||||
'Workflow parsed, but no configurable primitive inputs were found. Ensure you exported in API format.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const parseWorkflowAndUpdateNodes = (showToast = false, isNewImport = false) => {
|
||||
const wfString: string = config.COMFYUI_WORKFLOW ?? '';
|
||||
|
||||
if (showToast && wfString === lastKnownWorkflowString && !isNewImport) return;
|
||||
|
||||
if (wfString.trim() === '') {
|
||||
workflowNodesConfig = [];
|
||||
lastKnownWorkflowString = wfString;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(wfString);
|
||||
const isValidApiFormat = Object.values(parsed).some(
|
||||
(n: any) => n && typeof n === 'object' && n.class_type && n.inputs
|
||||
);
|
||||
|
||||
if (!isValidApiFormat) {
|
||||
workflowNodesConfig = [];
|
||||
if (showToast)
|
||||
toast.warning(
|
||||
$i18n.t(
|
||||
'Not a valid ComfyUI API Workflow JSON format. Make sure to export as API format from ComfyUI.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const reconcileWith = isNewImport ? [] : (config.COMFYUI_WORKFLOW_NODES ?? []);
|
||||
const ok = parseAndPopulateWorkflowNodes(parsed, reconcileWith, showToast);
|
||||
if (ok) lastKnownWorkflowString = wfString;
|
||||
} catch {
|
||||
workflowNodesConfig = [];
|
||||
if (showToast) toast.error($i18n.t('Invalid JSON syntax in ComfyUI Workflow.'));
|
||||
}
|
||||
};
|
||||
|
||||
let showComfyUIEditWorkflowEditor = false;
|
||||
let REQUIRED_EDIT_WORKFLOW_NODES = [
|
||||
{
|
||||
type: 'image',
|
||||
key: 'image',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'prompt',
|
||||
key: 'prompt',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'model',
|
||||
key: 'unet_name',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'width',
|
||||
key: 'width',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'height',
|
||||
key: 'height',
|
||||
node_ids: ''
|
||||
|
||||
let editWorkflowNodesConfig: {
|
||||
type: string;
|
||||
key: string;
|
||||
node_ids: string;
|
||||
class_type: string;
|
||||
}[] = [];
|
||||
let lastKnownEditWorkflowString: string | null = null;
|
||||
|
||||
const parseEditWorkflowAndUpdateNodes = (showToast = false, isNewImport = false) => {
|
||||
const wfString: string = config.IMAGES_EDIT_COMFYUI_WORKFLOW ?? '';
|
||||
|
||||
if (showToast && wfString === lastKnownEditWorkflowString && !isNewImport) return;
|
||||
|
||||
if (wfString.trim() === '') {
|
||||
editWorkflowNodesConfig = [];
|
||||
lastKnownEditWorkflowString = wfString;
|
||||
return;
|
||||
}
|
||||
];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(wfString);
|
||||
const isValidApiFormat = Object.values(parsed).some(
|
||||
(n: any) => n && typeof n === 'object' && n.class_type && n.inputs
|
||||
);
|
||||
|
||||
if (!isValidApiFormat) {
|
||||
editWorkflowNodesConfig = [];
|
||||
if (showToast)
|
||||
toast.warning(
|
||||
$i18n.t(
|
||||
'Not a valid ComfyUI API Workflow JSON format. Make sure to export as API format from ComfyUI.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const reconcileWith = isNewImport ? [] : (config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES ?? []);
|
||||
const savedNodes = reconcileWith;
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
editWorkflowNodesConfig = [];
|
||||
return;
|
||||
}
|
||||
const nodeGroups = new Map<
|
||||
string,
|
||||
{ type: string; key: string; node_ids: string[]; class_type: string }
|
||||
>();
|
||||
for (const nodeId of Object.keys(parsed)) {
|
||||
const node = parsed[nodeId];
|
||||
if (!node || typeof node !== 'object' || !node.inputs || typeof node.inputs !== 'object')
|
||||
continue;
|
||||
for (const inputKey of Object.keys(node.inputs)) {
|
||||
const val = node.inputs[inputKey];
|
||||
const valType = typeof val;
|
||||
if (valType !== 'string' && valType !== 'number' && valType !== 'boolean') continue;
|
||||
|
||||
const entryKey = `${nodeId}::${node.class_type}::${inputKey}`;
|
||||
const semanticType = `${node.class_type}::${inputKey}`;
|
||||
|
||||
if (!nodeGroups.has(entryKey)) {
|
||||
const ambiguousKeys = new Set(['text', 'prompt', 'positive']);
|
||||
const autoAssign = !ambiguousKeys.has(inputKey);
|
||||
|
||||
nodeGroups.set(entryKey, {
|
||||
type: semanticType,
|
||||
key: inputKey,
|
||||
node_ids: autoAssign ? [nodeId] : [],
|
||||
class_type: node.class_type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
editWorkflowNodesConfig = Array.from(nodeGroups.values()).map((n) => ({
|
||||
...n,
|
||||
node_ids: n.node_ids.join(',')
|
||||
}));
|
||||
|
||||
if (showToast && editWorkflowNodesConfig.length > 0) {
|
||||
toast.success(
|
||||
$i18n.t(
|
||||
`Workflow parsed. {{count}} configurable input(s) found. Please review the Node IDs.`,
|
||||
{ count: editWorkflowNodesConfig.length }
|
||||
)
|
||||
);
|
||||
}
|
||||
lastKnownEditWorkflowString = wfString;
|
||||
} catch {
|
||||
editWorkflowNodesConfig = [];
|
||||
if (showToast) toast.error($i18n.t('Invalid JSON syntax in ComfyUI Workflow.'));
|
||||
}
|
||||
};
|
||||
|
||||
const getModels = async () => {
|
||||
models = await getImageGenerationModels(localStorage.token).catch((error) => {
|
||||
|
|
@ -182,15 +342,11 @@
|
|||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
config.COMFYUI_WORKFLOW_NODES = REQUIRED_WORKFLOW_NODES.map((node) => {
|
||||
return {
|
||||
type: node.type,
|
||||
key: node.key,
|
||||
node_ids:
|
||||
node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
|
||||
};
|
||||
});
|
||||
config.COMFYUI_WORKFLOW_NODES = workflowNodesConfig.map((node) => ({
|
||||
type: node.type,
|
||||
key: node.key,
|
||||
node_ids: node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
|
||||
}));
|
||||
}
|
||||
|
||||
if (config?.IMAGES_EDIT_COMFYUI_WORKFLOW) {
|
||||
|
|
@ -199,15 +355,11 @@
|
|||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = REQUIRED_EDIT_WORKFLOW_NODES.map((node) => {
|
||||
return {
|
||||
type: node.type,
|
||||
key: node.key,
|
||||
node_ids:
|
||||
node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
|
||||
};
|
||||
});
|
||||
config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = editWorkflowNodesConfig.map((node) => ({
|
||||
type: node.type,
|
||||
key: node.key,
|
||||
node_ids: node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
|
||||
}));
|
||||
}
|
||||
|
||||
const res = await updateConfigHandler();
|
||||
|
|
@ -243,19 +395,9 @@
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
parseWorkflowAndUpdateNodes(false, false);
|
||||
}
|
||||
|
||||
REQUIRED_WORKFLOW_NODES = REQUIRED_WORKFLOW_NODES.map((node) => {
|
||||
const n = config.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
|
||||
console.debug(n);
|
||||
|
||||
return {
|
||||
type: n.type,
|
||||
key: n.key,
|
||||
node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
|
||||
};
|
||||
});
|
||||
|
||||
if (config.IMAGES_EDIT_COMFYUI_WORKFLOW) {
|
||||
try {
|
||||
config.IMAGES_EDIT_COMFYUI_WORKFLOW = JSON.stringify(
|
||||
|
|
@ -266,6 +408,7 @@
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
parseEditWorkflowAndUpdateNodes(false, false);
|
||||
}
|
||||
|
||||
config.IMAGES_OPENAI_API_PARAMS =
|
||||
|
|
@ -277,18 +420,6 @@
|
|||
typeof config.AUTOMATIC1111_PARAMS === 'object'
|
||||
? JSON.stringify(config.AUTOMATIC1111_PARAMS ?? {}, null, 2)
|
||||
: config.AUTOMATIC1111_PARAMS;
|
||||
|
||||
REQUIRED_EDIT_WORKFLOW_NODES = REQUIRED_EDIT_WORKFLOW_NODES.map((node) => {
|
||||
const n =
|
||||
config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
|
||||
console.debug(n);
|
||||
|
||||
return {
|
||||
type: n.type,
|
||||
key: n.key,
|
||||
node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -552,13 +683,13 @@
|
|||
accept=".json"
|
||||
on:change={(e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
config.COMFYUI_WORKFLOW = e.target.result;
|
||||
e.target.value = null;
|
||||
reader.onload = (ev) => {
|
||||
config.COMFYUI_WORKFLOW = ev.target.result as string;
|
||||
parseWorkflowAndUpdateNodes(true, true);
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -605,9 +736,10 @@
|
|||
lang="json"
|
||||
onChange={(e) => {
|
||||
config.COMFYUI_WORKFLOW = e;
|
||||
parseWorkflowAndUpdateNodes(false, false);
|
||||
}}
|
||||
onSave={() => {
|
||||
console.log('Saved');
|
||||
parseWorkflowAndUpdateNodes(true, false);
|
||||
}}
|
||||
/>
|
||||
<!-- {#if config.COMFYUI_WORKFLOW}
|
||||
|
|
@ -624,51 +756,74 @@
|
|||
{#if config.COMFYUI_WORKFLOW}
|
||||
<AdminSettingField
|
||||
label={$i18n.t('ComfyUI Workflow Nodes')}
|
||||
description={$i18n.t('Map workflow node inputs used for image generation.')}
|
||||
description={$i18n.t(
|
||||
'Node IDs are auto-detected from your workflow. Adjust them if needed.'
|
||||
)}
|
||||
>
|
||||
<div class="flex flex-col gap-1.5 text-xs">
|
||||
{#each REQUIRED_WORKFLOW_NODES as node}
|
||||
<div class="flex w-full flex-col">
|
||||
<div class="shrink-0">
|
||||
<div class=" capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500">
|
||||
{node.type}{node.type === 'prompt' ? '*' : ''}
|
||||
</div>
|
||||
</div>
|
||||
{#if workflowNodesConfig.length > 0}
|
||||
<div class="flex items-center gap-1 text-xs text-green-500 dark:text-green-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{$i18n.t('Auto-detected')}
|
||||
</div>
|
||||
|
||||
<div class="flex mt-0.5 items-center">
|
||||
<div class="">
|
||||
<Tooltip content={$i18n.t('Input Key (e.g. text, unet_name, steps)')}>
|
||||
<input
|
||||
class="{inputClass} w-24"
|
||||
placeholder={$i18n.t('Key')}
|
||||
bind:value={node.key}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="px-2 text-gray-400 dark:text-gray-500">:</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Tooltip
|
||||
content={$i18n.t('Comma separated Node Ids (e.g. 1 or 1,2)')}
|
||||
placement="top-start"
|
||||
<div class="mt-1 flex flex-col gap-1.5 text-xs">
|
||||
{#each workflowNodesConfig as node}
|
||||
<div class="flex w-full flex-col">
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class="capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500"
|
||||
title={node.type}
|
||||
>
|
||||
<input
|
||||
class={inputClass}
|
||||
placeholder={$i18n.t('Node Ids')}
|
||||
bind:value={node.node_ids}
|
||||
/>
|
||||
</Tooltip>
|
||||
{node.class_type}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-0.5 flex items-center">
|
||||
<div class="">
|
||||
<Tooltip content={$i18n.t('Input Key (e.g. text, unet_name, steps)')}>
|
||||
<input
|
||||
class="{inputClass} w-24"
|
||||
placeholder={$i18n.t('Key')}
|
||||
bind:value={node.key}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="px-2 text-gray-400 dark:text-gray-500">:</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Tooltip
|
||||
content={$i18n.t('Comma separated Node Ids (e.g. 1 or 1,2)')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class={inputClass}
|
||||
placeholder={$i18n.t('Node Ids')}
|
||||
bind:value={node.node_ids}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('*Prompt node ID(s) are required for image generation')}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('No configurable inputs detected. Upload a workflow in API format.')}
|
||||
</div>
|
||||
{/if}
|
||||
</AdminSettingField>
|
||||
{/if}
|
||||
{:else if config?.IMAGE_GENERATION_ENGINE === 'gemini'}
|
||||
|
|
@ -850,13 +1005,13 @@
|
|||
accept=".json"
|
||||
on:change={(e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
config.IMAGES_EDIT_COMFYUI_WORKFLOW = e.target.result;
|
||||
e.target.value = null;
|
||||
reader.onload = (ev) => {
|
||||
config.IMAGES_EDIT_COMFYUI_WORKFLOW = ev.target.result as string;
|
||||
parseEditWorkflowAndUpdateNodes(true, true);
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -902,9 +1057,10 @@
|
|||
lang="json"
|
||||
onChange={(e) => {
|
||||
config.IMAGES_EDIT_COMFYUI_WORKFLOW = e;
|
||||
parseEditWorkflowAndUpdateNodes(false, false);
|
||||
}}
|
||||
onSave={() => {
|
||||
console.log('Saved');
|
||||
parseEditWorkflowAndUpdateNodes(true, false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -912,51 +1068,74 @@
|
|||
{#if config.IMAGES_EDIT_COMFYUI_WORKFLOW}
|
||||
<AdminSettingField
|
||||
label={$i18n.t('ComfyUI Workflow Nodes')}
|
||||
description={$i18n.t('Map workflow node inputs used for image edits.')}
|
||||
description={$i18n.t(
|
||||
'Node IDs are auto-detected from your workflow. Adjust them if needed.'
|
||||
)}
|
||||
>
|
||||
<div class="flex flex-col gap-1.5 text-xs">
|
||||
{#each REQUIRED_EDIT_WORKFLOW_NODES as node}
|
||||
<div class="flex w-full flex-col">
|
||||
<div class="shrink-0">
|
||||
<div class=" capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500">
|
||||
{node.type}{['prompt', 'image'].includes(node.type) ? '*' : ''}
|
||||
</div>
|
||||
</div>
|
||||
{#if editWorkflowNodesConfig.length > 0}
|
||||
<div class="flex items-center gap-1 text-xs text-green-500 dark:text-green-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{$i18n.t('Auto-detected')}
|
||||
</div>
|
||||
|
||||
<div class="flex mt-0.5 items-center">
|
||||
<div class="">
|
||||
<Tooltip content={$i18n.t('Input Key (e.g. text, unet_name, steps)')}>
|
||||
<input
|
||||
class="{inputClass} w-24"
|
||||
placeholder={$i18n.t('Key')}
|
||||
bind:value={node.key}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="px-2 text-gray-400 dark:text-gray-500">:</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Tooltip
|
||||
content={$i18n.t('Comma separated Node Ids (e.g. 1 or 1,2)')}
|
||||
placement="top-start"
|
||||
<div class="mt-1 flex flex-col gap-1.5 text-xs">
|
||||
{#each editWorkflowNodesConfig as node}
|
||||
<div class="flex w-full flex-col">
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class="capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500"
|
||||
title={node.type}
|
||||
>
|
||||
<input
|
||||
class={inputClass}
|
||||
placeholder={$i18n.t('Node Ids')}
|
||||
bind:value={node.node_ids}
|
||||
/>
|
||||
</Tooltip>
|
||||
{node.class_type}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-0.5 flex items-center">
|
||||
<div class="">
|
||||
<Tooltip content={$i18n.t('Input Key (e.g. text, unet_name, steps)')}>
|
||||
<input
|
||||
class="{inputClass} w-24"
|
||||
placeholder={$i18n.t('Key')}
|
||||
bind:value={node.key}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="px-2 text-gray-400 dark:text-gray-500">:</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Tooltip
|
||||
content={$i18n.t('Comma separated Node Ids (e.g. 1 or 1,2)')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class={inputClass}
|
||||
placeholder={$i18n.t('Node Ids')}
|
||||
bind:value={node.node_ids}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('*Prompt node ID(s) are required for image generation')}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('No configurable inputs detected. Upload a workflow in API format.')}
|
||||
</div>
|
||||
{/if}
|
||||
</AdminSettingField>
|
||||
{/if}
|
||||
{:else if config?.IMAGE_EDIT_ENGINE === 'gemini'}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@
|
|||
"{{NAMES}} reacted with {{REACTION}}": "",
|
||||
"{{user}}'s Chats": "",
|
||||
"{{webUIName}} Backend Required": "",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"1 file": "",
|
||||
"1 group": "",
|
||||
"1 hour before": "",
|
||||
|
|
@ -319,6 +318,7 @@
|
|||
"Auto Redirect": "",
|
||||
"Auto-Copy Response to Clipboard": "",
|
||||
"Auto-Create Groups": "",
|
||||
"Auto-detected": "",
|
||||
"Auto-Playback Response": "",
|
||||
"Autocomplete Generation": "",
|
||||
"Autocomplete Generation Input Max Length": "",
|
||||
|
|
@ -1117,6 +1117,7 @@
|
|||
"Error accessing Google Drive: {{error}}": "",
|
||||
"Error accessing media devices.": "",
|
||||
"Error deleting model: {{error}}": "",
|
||||
"Error occurred during workflow parsing.": "",
|
||||
"Error starting recording.": "",
|
||||
"Error unloading model: {{error}}": "",
|
||||
"Error uploading file: {{error}}": "",
|
||||
|
|
@ -1501,6 +1502,8 @@
|
|||
"Invalid JSON format in {{NAME}}": "",
|
||||
"Invalid JSON format in Additional Config": "",
|
||||
"Invalid JSON format in MinerU Parameters": "",
|
||||
"Invalid JSON syntax in ComfyUI Workflow.": "",
|
||||
"Invalid workflow data provided for parsing.": "",
|
||||
"is typing...": "",
|
||||
"Italic": "",
|
||||
"January": "",
|
||||
|
|
@ -1642,8 +1645,6 @@
|
|||
"Map LDAP groups to Open WebUI groups.": "",
|
||||
"Map OAuth claims to Open WebUI groups.": "",
|
||||
"Map OAuth claims to Open WebUI roles.": "",
|
||||
"Map workflow node inputs used for image edits.": "",
|
||||
"Map workflow node inputs used for image generation.": "",
|
||||
"Mapped Source": "",
|
||||
"March": "",
|
||||
"Mark all as read": "",
|
||||
|
|
@ -1848,6 +1849,7 @@
|
|||
"No chats found": "",
|
||||
"No chats found for this user.": "",
|
||||
"No chats found.": "",
|
||||
"No configurable inputs detected. Upload a workflow in API format.": "",
|
||||
"No content": "",
|
||||
"No content found": "",
|
||||
"No content to speak": "",
|
||||
|
|
@ -1911,7 +1913,9 @@
|
|||
"No valves to update": "",
|
||||
"No webhooks yet": "",
|
||||
"Node Ids": "",
|
||||
"Node IDs are auto-detected from your workflow. Adjust them if needed.": "",
|
||||
"None": "",
|
||||
"Not a valid ComfyUI API Workflow JSON format. Make sure to export as API format from ComfyUI.": "",
|
||||
"Not configured": "",
|
||||
"Not factually correct": "",
|
||||
"Not helpful": "",
|
||||
|
|
@ -3037,6 +3041,9 @@
|
|||
"Width in pixels to compress images to. Leave empty for no compression.": "",
|
||||
"Wikipedia": "",
|
||||
"Won": "",
|
||||
"Workflow parsed, but no configurable primitive inputs were found. Ensure you exported in API format.": "",
|
||||
"Workflow parsed. {{count}} configurable input(s) found. Please review the Node IDs._one": "",
|
||||
"Workflow parsed. {{count}} configurable input(s) found. Please review the Node IDs._other": "",
|
||||
"Working Directory": "",
|
||||
"Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text.": "",
|
||||
"Workspace": "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue