[bugfix] Fix PNG loading with NaN values in metadata

Restore lazy evaluation for metadata parsing to handle PNG files with NaN values in prompt field. Uses the original if-else loading logic to prevent parsing errors when workflow is valid but prompt contains invalid JSON.

Fixes #4199
This commit is contained in:
bymyself
2025-06-16 15:11:34 -07:00
parent 3b435e337e
commit f035cbcbd5
3 changed files with 739 additions and 158 deletions

View File

@@ -30,12 +30,6 @@ import {
isComboInputSpecV1,
isComboInputSpecV2
} from '@/schemas/nodeDefSchema'
import { getFromWebmFile } from '@/scripts/metadata/ebml'
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
import { getMp3Metadata } from '@/scripts/metadata/mp3'
import { getOggMetadata } from '@/scripts/metadata/ogg'
import { getSvgMetadata } from '@/scripts/metadata/svg'
import { useDialogService } from '@/services/dialogService'
import { useExtensionService } from '@/services/extensionService'
import { useLitegraphService } from '@/services/litegraphService'
@@ -60,6 +54,7 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
import { ExtensionManager } from '@/types/extensionTypes'
import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil'
import { graphToPrompt } from '@/utils/executionUtil'
import { getFileHandler } from '@/utils/fileHandlers'
import {
executeWidgetsCallback,
fixLinkInputSlots,
@@ -74,13 +69,7 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard'
import { type ComfyApi, PromptExecutionError, api } from './api'
import { defaultGraph } from './defaultGraph'
import {
getFlacMetadata,
getLatentMetadata,
getPngMetadata,
getWebpMetadata,
importA1111
} from './pnginfo'
import { importA1111 } from './pnginfo'
import { $el, ComfyUI } from './ui'
import { ComfyAppMenu } from './ui/menu/index'
import { clone } from './utils'
@@ -1332,160 +1321,58 @@ export class ComfyApp {
return f.substring(0, p)
}
const fileName = removeExt(file.name)
if (file.type === 'image/png') {
const pngInfo = await getPngMetadata(file)
if (pngInfo?.workflow) {
const handler = getFileHandler(file)
if (!handler) {
this.showErrorOnFileLoad(file)
return
}
const metadata = await handler(file)
// Special handling for JSON files
if (metadata.jsonTemplateData) {
const jsonContent = metadata.jsonTemplateData()
if (
jsonContent &&
typeof jsonContent === 'object' &&
'templates' in jsonContent
) {
this.loadTemplateData(jsonContent as any)
return
} else if (this.isApiJson(jsonContent)) {
this.loadApiJson(jsonContent as ComfyApiWorkflow, fileName)
return
} else {
// Regular workflow JSON
await this.loadGraphData(
JSON.parse(pngInfo.workflow),
jsonContent as ComfyWorkflowJSON,
true,
true,
fileName
)
} else if (pngInfo?.prompt) {
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName)
} else if (pngInfo?.parameters) {
// Note: Not putting this in `importA1111` as it is mostly not used
// by external callers, and `importA1111` has no access to `app`.
useWorkflowService().beforeLoadNewGraph()
importA1111(this.graph, pngInfo.parameters)
useWorkflowService().afterLoadNewGraph(
fileName,
this.graph.serialize() as unknown as ComfyWorkflowJSON
)
} else {
this.showErrorOnFileLoad(file)
return
}
} else if (file.type === 'image/webp') {
const pngInfo = await getWebpMetadata(file)
// Support loading workflows from that webp custom node.
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
}
if (workflow) {
this.loadGraphData(JSON.parse(workflow), true, true, fileName)
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt), fileName)
} else {
this.showErrorOnFileLoad(file)
if (metadata.workflow) {
const workflowData = metadata.workflow()
if (workflowData) {
await this.loadGraphData(workflowData, true, true, fileName)
}
} else if (file.type === 'audio/mpeg') {
const { workflow, prompt } = await getMp3Metadata(file)
if (workflow) {
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/ogg') {
const { workflow, prompt } = await getOggMetadata(file)
if (workflow) {
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/flac' || file.type === 'audio/x-flac') {
const pngInfo = await getFlacMetadata(file)
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
if (workflow) {
this.loadGraphData(JSON.parse(workflow), true, true, fileName)
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt), fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'video/webm') {
const webmInfo = await getFromWebmFile(file)
if (webmInfo.workflow) {
this.loadGraphData(webmInfo.workflow, true, true, fileName)
} else if (webmInfo.prompt) {
this.loadApiJson(webmInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (
file.type === 'video/mp4' ||
file.name?.endsWith('.mp4') ||
file.name?.endsWith('.mov') ||
file.name?.endsWith('.m4v') ||
file.type === 'video/quicktime' ||
file.type === 'video/x-m4v'
) {
const mp4Info = await getFromIsobmffFile(file)
if (mp4Info.workflow) {
this.loadGraphData(mp4Info.workflow, true, true, fileName)
} else if (mp4Info.prompt) {
this.loadApiJson(mp4Info.prompt, fileName)
}
} else if (file.type === 'image/svg+xml' || file.name?.endsWith('.svg')) {
const svgInfo = await getSvgMetadata(file)
if (svgInfo.workflow) {
this.loadGraphData(svgInfo.workflow, true, true, fileName)
} else if (svgInfo.prompt) {
this.loadApiJson(svgInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (
file.type === 'model/gltf-binary' ||
file.name?.endsWith('.glb')
) {
const gltfInfo = await getGltfBinaryMetadata(file)
if (gltfInfo.workflow) {
this.loadGraphData(gltfInfo.workflow, true, true, fileName)
} else if (gltfInfo.prompt) {
this.loadApiJson(gltfInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (
file.type === 'application/json' ||
file.name?.endsWith('.json')
) {
const reader = new FileReader()
reader.onload = async () => {
const readerResult = reader.result as string
const jsonContent = JSON.parse(readerResult)
if (jsonContent?.templates) {
this.loadTemplateData(jsonContent)
} else if (this.isApiJson(jsonContent)) {
this.loadApiJson(jsonContent, fileName)
} else {
await this.loadGraphData(
JSON.parse(readerResult),
true,
true,
fileName
)
}
}
reader.readAsText(file)
} else if (
file.name?.endsWith('.latent') ||
file.name?.endsWith('.safetensors')
) {
const info = await getLatentMetadata(file)
// TODO define schema to LatentMetadata
// @ts-expect-error
if (info.workflow) {
await this.loadGraphData(
// @ts-expect-error
JSON.parse(info.workflow),
true,
true,
fileName
)
// @ts-expect-error
} else if (info.prompt) {
// @ts-expect-error
this.loadApiJson(JSON.parse(info.prompt))
} else {
this.showErrorOnFileLoad(file)
} else if (metadata.prompt) {
const promptData = metadata.prompt()
if (promptData) {
this.loadApiJson(promptData, fileName)
}
} else if (metadata.parameters) {
// A1111 import
useWorkflowService().beforeLoadNewGraph()
importA1111(this.graph, metadata.parameters)
useWorkflowService().afterLoadNewGraph(
fileName,
this.graph.serialize() as unknown as ComfyWorkflowJSON
)
} else {
this.showErrorOnFileLoad(file)
}

334
src/utils/fileHandlers.ts Normal file
View File

@@ -0,0 +1,334 @@
/**
* Maps MIME types and file extensions to handler functions for extracting
* workflow data from various file formats. Uses supportedWorkflowFormats.ts
* as the source of truth for supported formats.
*/
import {
AUDIO_WORKFLOW_FORMATS,
DATA_WORKFLOW_FORMATS,
IMAGE_WORKFLOW_FORMATS,
MODEL_WORKFLOW_FORMATS,
VIDEO_WORKFLOW_FORMATS
} from '@/constants/supportedWorkflowFormats'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON
} from '@/schemas/comfyWorkflowSchema'
import { getFromWebmFile } from '@/scripts/metadata/ebml'
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
import { getMp3Metadata } from '@/scripts/metadata/mp3'
import { getOggMetadata } from '@/scripts/metadata/ogg'
import { getSvgMetadata } from '@/scripts/metadata/svg'
import {
getFlacMetadata,
getLatentMetadata,
getPngMetadata,
getWebpMetadata
} from '@/scripts/pnginfo'
/**
* Type for the raw file metadata with lazy parsing
*/
export type WorkflowFileMetadata = {
workflow?: () => ComfyWorkflowJSON | undefined
prompt?: () => ComfyApiWorkflow | undefined
parameters?: string
jsonTemplateData?: () => unknown
}
/**
* Type for the file handler function
*/
export type WorkflowFileHandler = (file: File) => Promise<WorkflowFileMetadata>
/**
* Maps MIME types to file handlers for loading workflows from different file formats
*/
export const mimeTypeHandlers = new Map<string, WorkflowFileHandler>()
/**
* Maps file extensions to file handlers for loading workflows
* Used as a fallback when MIME type detection fails
*/
export const extensionHandlers = new Map<string, WorkflowFileHandler>()
/**
* Handler for PNG files
*/
const handlePngFile: WorkflowFileHandler = async (file) => {
const pngInfo = await getPngMetadata(file)
return {
workflow: () =>
pngInfo?.workflow ? JSON.parse(pngInfo.workflow) : undefined,
prompt: () => (pngInfo?.prompt ? JSON.parse(pngInfo.prompt) : undefined),
parameters: pngInfo?.parameters
}
}
/**
* Handler for WebP files
*/
const handleWebpFile: WorkflowFileHandler = async (file) => {
const pngInfo = await getWebpMetadata(file)
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
return {
workflow: () => (workflow ? JSON.parse(workflow) : undefined),
prompt: () => (prompt ? JSON.parse(prompt) : undefined)
}
}
/**
* Handler for SVG files
*/
const handleSvgFile: WorkflowFileHandler = async (file) => {
const svgInfo = await getSvgMetadata(file)
return {
workflow: () => svgInfo.workflow,
prompt: () => svgInfo.prompt
}
}
/**
* Handler for MP3 files
*/
const handleMp3File: WorkflowFileHandler = async (file) => {
const { workflow, prompt } = await getMp3Metadata(file)
return {
workflow: () => workflow,
prompt: () => prompt
}
}
/**
* Handler for OGG files
*/
const handleOggFile: WorkflowFileHandler = async (file) => {
const { workflow, prompt } = await getOggMetadata(file)
return {
workflow: () => workflow,
prompt: () => prompt
}
}
/**
* Handler for FLAC files
*/
const handleFlacFile: WorkflowFileHandler = async (file) => {
const pngInfo = await getFlacMetadata(file)
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
return {
workflow: () => (workflow ? JSON.parse(workflow) : undefined),
prompt: () => (prompt ? JSON.parse(prompt) : undefined)
}
}
/**
* Handler for WebM files
*/
const handleWebmFile: WorkflowFileHandler = async (file) => {
const webmInfo = await getFromWebmFile(file)
return {
workflow: () => webmInfo.workflow,
prompt: () => webmInfo.prompt
}
}
/**
* Handler for MP4/MOV/M4V files
*/
const handleMp4File: WorkflowFileHandler = async (file) => {
const mp4Info = await getFromIsobmffFile(file)
return {
workflow: () => mp4Info.workflow,
prompt: () => mp4Info.prompt
}
}
/**
* Handler for GLB files
*/
const handleGlbFile: WorkflowFileHandler = async (file) => {
const gltfInfo = await getGltfBinaryMetadata(file)
return {
workflow: () => gltfInfo.workflow,
prompt: () => gltfInfo.prompt
}
}
/**
* Handler for JSON files
*/
const handleJsonFile: WorkflowFileHandler = async (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
try {
const readerResult = reader.result as string
resolve({
workflow: () => JSON.parse(readerResult),
prompt: () => JSON.parse(readerResult),
jsonTemplateData: () => JSON.parse(readerResult)
})
} catch (error) {
reject(error)
}
}
reader.onerror = () => reject(reader.error)
reader.readAsText(file)
})
}
/**
* Handler for .latent and .safetensors files
*/
const handleLatentFile: WorkflowFileHandler = async (file) => {
const info = await getLatentMetadata(file)
return {
workflow: () => {
if (
info &&
typeof info === 'object' &&
'workflow' in info &&
info.workflow
) {
return JSON.parse(info.workflow as string)
}
return undefined
},
prompt: () => {
if (info && typeof info === 'object' && 'prompt' in info && info.prompt) {
return JSON.parse(info.prompt as string)
}
return undefined
}
}
}
/**
* Helper function to determine if a JSON object is in the API JSON format
*/
export function isApiJson(data: unknown) {
return (
typeof data === 'object' &&
data !== null &&
Object.keys(data).length > 0 &&
Object.values(data as Record<string, any>).every((v) => v.class_type)
)
}
// Register image format handlers
IMAGE_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
if (mimeType === 'image/png') {
mimeTypeHandlers.set(mimeType, handlePngFile)
} else if (mimeType === 'image/webp') {
mimeTypeHandlers.set(mimeType, handleWebpFile)
} else if (mimeType === 'image/svg+xml') {
mimeTypeHandlers.set(mimeType, handleSvgFile)
}
})
IMAGE_WORKFLOW_FORMATS.extensions.forEach((ext) => {
if (ext === '.png') {
extensionHandlers.set(ext, handlePngFile)
} else if (ext === '.webp') {
extensionHandlers.set(ext, handleWebpFile)
} else if (ext === '.svg') {
extensionHandlers.set(ext, handleSvgFile)
}
})
// Register audio format handlers
AUDIO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
if (mimeType === 'audio/mpeg') {
mimeTypeHandlers.set(mimeType, handleMp3File)
} else if (mimeType === 'audio/ogg') {
mimeTypeHandlers.set(mimeType, handleOggFile)
} else if (mimeType === 'audio/flac' || mimeType === 'audio/x-flac') {
mimeTypeHandlers.set(mimeType, handleFlacFile)
}
})
AUDIO_WORKFLOW_FORMATS.extensions.forEach((ext) => {
if (ext === '.mp3') {
extensionHandlers.set(ext, handleMp3File)
} else if (ext === '.ogg') {
extensionHandlers.set(ext, handleOggFile)
} else if (ext === '.flac') {
extensionHandlers.set(ext, handleFlacFile)
}
})
// Register video format handlers
VIDEO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
if (mimeType === 'video/webm') {
mimeTypeHandlers.set(mimeType, handleWebmFile)
} else if (
mimeType === 'video/mp4' ||
mimeType === 'video/quicktime' ||
mimeType === 'video/x-m4v'
) {
mimeTypeHandlers.set(mimeType, handleMp4File)
}
})
VIDEO_WORKFLOW_FORMATS.extensions.forEach((ext) => {
if (ext === '.webm') {
extensionHandlers.set(ext, handleWebmFile)
} else if (ext === '.mp4' || ext === '.mov' || ext === '.m4v') {
extensionHandlers.set(ext, handleMp4File)
}
})
// Register 3D model format handlers
MODEL_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
if (mimeType === 'model/gltf-binary') {
mimeTypeHandlers.set(mimeType, handleGlbFile)
}
})
MODEL_WORKFLOW_FORMATS.extensions.forEach((ext) => {
if (ext === '.glb') {
extensionHandlers.set(ext, handleGlbFile)
}
})
// Register data format handlers
DATA_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
if (mimeType === 'application/json') {
mimeTypeHandlers.set(mimeType, handleJsonFile)
}
})
DATA_WORKFLOW_FORMATS.extensions.forEach((ext) => {
if (ext === '.json') {
extensionHandlers.set(ext, handleJsonFile)
} else if (ext === '.latent' || ext === '.safetensors') {
extensionHandlers.set(ext, handleLatentFile)
}
})
/**
* Gets the appropriate file handler for a given file based on mime type or extension
*/
export function getFileHandler(file: File): WorkflowFileHandler | null {
// First try to match by MIME type
if (file.type && mimeTypeHandlers.has(file.type)) {
return mimeTypeHandlers.get(file.type) || null
}
// If no MIME type match, try to match by file extension
if (file.name) {
const extension = '.' + file.name.split('.').pop()?.toLowerCase()
if (extension && extensionHandlers.has(extension)) {
return extensionHandlers.get(extension) || null
}
}
return null
}