mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
[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:
@@ -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
334
src/utils/fileHandlers.ts
Normal 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
|
||||
}
|
||||
360
tests-ui/tests/utils/fileHandlers.test.ts
Normal file
360
tests-ui/tests/utils/fileHandlers.test.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
AUDIO_WORKFLOW_FORMATS,
|
||||
DATA_WORKFLOW_FORMATS,
|
||||
IMAGE_WORKFLOW_FORMATS,
|
||||
MODEL_WORKFLOW_FORMATS,
|
||||
VIDEO_WORKFLOW_FORMATS
|
||||
} from '../../../src/constants/supportedWorkflowFormats'
|
||||
import {
|
||||
extensionHandlers,
|
||||
getFileHandler,
|
||||
isApiJson,
|
||||
mimeTypeHandlers
|
||||
} from '../../../src/utils/fileHandlers'
|
||||
|
||||
// Mock the metadata functions
|
||||
vi.mock('../../../src/scripts/pnginfo', () => ({
|
||||
getPngMetadata: vi.fn().mockResolvedValue({
|
||||
workflow: '{"test": "workflow"}',
|
||||
prompt: '{"test": "prompt"}'
|
||||
}),
|
||||
getWebpMetadata: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ workflow: '{"test": "workflow"}' }),
|
||||
getFlacMetadata: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ workflow: '{"test": "workflow"}' }),
|
||||
getLatentMetadata: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ workflow: '{"test": "workflow"}' })
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/scripts/metadata/svg', () => ({
|
||||
getSvgMetadata: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/scripts/metadata/mp3', () => ({
|
||||
getMp3Metadata: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/scripts/metadata/ogg', () => ({
|
||||
getOggMetadata: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/scripts/metadata/ebml', () => ({
|
||||
getFromWebmFile: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/scripts/metadata/isobmff', () => ({
|
||||
getFromIsobmffFile: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ workflow: { test: 'workflow' } })
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/scripts/metadata/gltf', () => ({
|
||||
getGltfBinaryMetadata: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ workflow: { test: 'workflow' } })
|
||||
}))
|
||||
|
||||
describe('fileHandlers', () => {
|
||||
describe('handler registrations', () => {
|
||||
it('should register handlers for all image MIME types', () => {
|
||||
IMAGE_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
|
||||
expect(mimeTypeHandlers.has(mimeType)).toBe(true)
|
||||
expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all image extensions', () => {
|
||||
IMAGE_WORKFLOW_FORMATS.extensions.forEach((ext) => {
|
||||
expect(extensionHandlers.has(ext)).toBe(true)
|
||||
expect(extensionHandlers.get(ext)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all audio MIME types', () => {
|
||||
AUDIO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
|
||||
expect(mimeTypeHandlers.has(mimeType)).toBe(true)
|
||||
expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all audio extensions', () => {
|
||||
AUDIO_WORKFLOW_FORMATS.extensions.forEach((ext) => {
|
||||
expect(extensionHandlers.has(ext)).toBe(true)
|
||||
expect(extensionHandlers.get(ext)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all video MIME types', () => {
|
||||
VIDEO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
|
||||
expect(mimeTypeHandlers.has(mimeType)).toBe(true)
|
||||
expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all video extensions', () => {
|
||||
VIDEO_WORKFLOW_FORMATS.extensions.forEach((ext) => {
|
||||
expect(extensionHandlers.has(ext)).toBe(true)
|
||||
expect(extensionHandlers.get(ext)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all model MIME types', () => {
|
||||
MODEL_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
|
||||
expect(mimeTypeHandlers.has(mimeType)).toBe(true)
|
||||
expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all model extensions', () => {
|
||||
MODEL_WORKFLOW_FORMATS.extensions.forEach((ext) => {
|
||||
expect(extensionHandlers.has(ext)).toBe(true)
|
||||
expect(extensionHandlers.get(ext)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all data MIME types', () => {
|
||||
DATA_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => {
|
||||
expect(mimeTypeHandlers.has(mimeType)).toBe(true)
|
||||
expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should register handlers for all data extensions', () => {
|
||||
DATA_WORKFLOW_FORMATS.extensions.forEach((ext) => {
|
||||
expect(extensionHandlers.has(ext)).toBe(true)
|
||||
expect(extensionHandlers.get(ext)).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFileHandler', () => {
|
||||
it('should return handler based on MIME type', () => {
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
expect(handler).toBeTruthy()
|
||||
expect(handler).toBeTypeOf('function')
|
||||
})
|
||||
|
||||
it('should return handler based on file extension when MIME type is not available', () => {
|
||||
const file = new File([''], 'test.png', { type: '' })
|
||||
const handler = getFileHandler(file)
|
||||
expect(handler).toBeTruthy()
|
||||
expect(handler).toBeTypeOf('function')
|
||||
})
|
||||
|
||||
it('should return null for unsupported file types', () => {
|
||||
const file = new File([''], 'test.xyz', { type: 'application/unknown' })
|
||||
const handler = getFileHandler(file)
|
||||
expect(handler).toBeNull()
|
||||
})
|
||||
|
||||
it('should prioritize MIME type over file extension', () => {
|
||||
const file = new File([''], 'test.txt', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
expect(handler).toBe(mimeTypeHandlers.get('image/png'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('isApiJson', () => {
|
||||
it('should return true for valid API JSON', () => {
|
||||
const apiJson = {
|
||||
'1': { class_type: 'Node1', inputs: {} },
|
||||
'2': { class_type: 'Node2', inputs: {} }
|
||||
}
|
||||
expect(isApiJson(apiJson)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for non-API JSON', () => {
|
||||
expect(isApiJson({})).toBe(false)
|
||||
expect(isApiJson({ nodes: [] })).toBe(false)
|
||||
expect(isApiJson(null)).toBe(false)
|
||||
expect(isApiJson('string')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PNG handler with NaN values', () => {
|
||||
it('should return lazy functions that parse on demand', async () => {
|
||||
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
|
||||
vi.mocked(getPngMetadata).mockResolvedValueOnce({
|
||||
workflow: '{"valid": "json"}',
|
||||
prompt: '{"invalid": NaN}'
|
||||
})
|
||||
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
expect(result.workflow).toBeTypeOf('function')
|
||||
expect(result.prompt).toBeTypeOf('function')
|
||||
|
||||
// workflow should parse successfully
|
||||
expect(result.workflow!()).toEqual({ valid: 'json' })
|
||||
|
||||
// prompt should throw on parse due to NaN
|
||||
expect(() => result.prompt!()).toThrow()
|
||||
})
|
||||
|
||||
it('should handle valid workflow with invalid prompt JSON', async () => {
|
||||
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
|
||||
vi.mocked(getPngMetadata).mockResolvedValueOnce({
|
||||
workflow: '{"nodes": [{"id": 1, "type": "TestNode"}]}',
|
||||
prompt:
|
||||
'{"1": {"inputs": {"seed": 123}, "class_type": "TestNode"}, "error": NaN}'
|
||||
})
|
||||
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
// workflow should parse successfully
|
||||
const workflowData = result.workflow!()
|
||||
expect(workflowData).toEqual({
|
||||
nodes: [{ id: 1, type: 'TestNode' }]
|
||||
})
|
||||
|
||||
// prompt should throw due to invalid JSON
|
||||
expect(() => result.prompt!()).toThrow('Unexpected token')
|
||||
})
|
||||
|
||||
it('should handle both fields containing NaN', async () => {
|
||||
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
|
||||
vi.mocked(getPngMetadata).mockResolvedValueOnce({
|
||||
workflow: '{"value": NaN}',
|
||||
prompt: '{"error": NaN}'
|
||||
})
|
||||
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
// Both should throw when called
|
||||
expect(() => result.workflow!()).toThrow()
|
||||
expect(() => result.prompt!()).toThrow()
|
||||
})
|
||||
|
||||
it('should handle missing metadata gracefully', async () => {
|
||||
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
|
||||
vi.mocked(getPngMetadata).mockResolvedValueOnce({})
|
||||
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
expect(result.workflow!()).toBeUndefined()
|
||||
expect(result.prompt!()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle undefined metadata fields', async () => {
|
||||
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
|
||||
vi.mocked(getPngMetadata).mockResolvedValueOnce({
|
||||
workflow: undefined as any,
|
||||
prompt: undefined as any,
|
||||
parameters: 'some parameters'
|
||||
})
|
||||
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
expect(result.workflow!()).toBeUndefined()
|
||||
expect(result.prompt!()).toBeUndefined()
|
||||
expect(result.parameters).toBe('some parameters')
|
||||
})
|
||||
})
|
||||
|
||||
describe('WebP handler variations', () => {
|
||||
it('should handle case-sensitive workflow field names', async () => {
|
||||
const { getWebpMetadata } = await import('../../../src/scripts/pnginfo')
|
||||
vi.mocked(getWebpMetadata).mockResolvedValueOnce({
|
||||
Workflow: '{"uppercase": true}',
|
||||
Prompt: '{"uppercase": true}'
|
||||
})
|
||||
|
||||
const file = new File([''], 'test.webp', { type: 'image/webp' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
expect(result.workflow!()).toEqual({ uppercase: true })
|
||||
expect(result.prompt!()).toEqual({ uppercase: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON handler edge cases', () => {
|
||||
it('should handle empty JSON file', async () => {
|
||||
const file = new File(['{}'], 'empty.json', { type: 'application/json' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
expect(result.jsonTemplateData!()).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle malformed JSON', async () => {
|
||||
const file = new File(['{invalid json}'], 'bad.json', {
|
||||
type: 'application/json'
|
||||
})
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
// Should throw when calling the lazy functions
|
||||
expect(() => result.jsonTemplateData!()).toThrow()
|
||||
expect(() => result.workflow!()).toThrow()
|
||||
expect(() => result.prompt!()).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Lazy evaluation behavior', () => {
|
||||
it('should not parse until functions are called', async () => {
|
||||
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
|
||||
const originalParse = JSON.parse
|
||||
let parseCallCount = 0
|
||||
JSON.parse = vi.fn((text: string) => {
|
||||
parseCallCount++
|
||||
return originalParse(text)
|
||||
})
|
||||
|
||||
vi.mocked(getPngMetadata).mockResolvedValueOnce({
|
||||
workflow: '{"test": "data"}',
|
||||
prompt: '{"test": "prompt"}'
|
||||
})
|
||||
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const handler = getFileHandler(file)
|
||||
const result = await handler!(file)
|
||||
|
||||
// JSON.parse should not have been called yet
|
||||
expect(parseCallCount).toBe(0)
|
||||
|
||||
// Call workflow function
|
||||
result.workflow!()
|
||||
expect(parseCallCount).toBe(1)
|
||||
|
||||
// Call prompt function
|
||||
result.prompt!()
|
||||
expect(parseCallCount).toBe(2)
|
||||
|
||||
// Restore original JSON.parse
|
||||
JSON.parse = originalParse
|
||||
})
|
||||
})
|
||||
|
||||
describe('File extension fallback', () => {
|
||||
it('should use file extension when MIME type is empty', () => {
|
||||
const file = new File([''], 'test.png', { type: '' })
|
||||
const handler = getFileHandler(file)
|
||||
expect(handler).toBe(extensionHandlers.get('.png'))
|
||||
})
|
||||
|
||||
it('should use file extension when MIME type is application/octet-stream', () => {
|
||||
const file = new File([''], 'test.mp3', {
|
||||
type: 'application/octet-stream'
|
||||
})
|
||||
const handler = getFileHandler(file)
|
||||
expect(handler).toBe(extensionHandlers.get('.mp3'))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user