mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-02 19:49:58 +00:00
Load workflows from GLTF files (#3169)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
import { type ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { getFromWebmFile } from '@/scripts/metadata/ebml'
|
||||
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
@@ -1452,6 +1453,15 @@ export class ComfyApp {
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
}
|
||||
} else if (file.type === 'model/gltf-binary') {
|
||||
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')
|
||||
|
||||
170
src/scripts/metadata/gltf.ts
Normal file
170
src/scripts/metadata/gltf.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
ComfyApiWorkflow,
|
||||
ComfyWorkflowJSON
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import {
|
||||
ASCII,
|
||||
ComfyMetadata,
|
||||
ComfyMetadataTags,
|
||||
GltfChunkHeader,
|
||||
GltfHeader,
|
||||
GltfJsonData,
|
||||
GltfSizeBytes
|
||||
} from '@/types/metadataTypes'
|
||||
|
||||
const MAX_READ_BYTES = 1 << 20
|
||||
|
||||
const isJsonChunk = (chunk: GltfChunkHeader | null): boolean =>
|
||||
!!chunk && chunk.chunkTypeIdentifier === ASCII.JSON
|
||||
|
||||
const isValidChunkRange = (
|
||||
start: number,
|
||||
length: number,
|
||||
bufferSize: number
|
||||
): boolean => start + length <= bufferSize
|
||||
|
||||
const byteArrayToString = (bytes: Uint8Array): string =>
|
||||
new TextDecoder().decode(bytes)
|
||||
|
||||
const parseGltfBinaryHeader = (dataView: DataView): GltfHeader | null => {
|
||||
if (dataView.byteLength < GltfSizeBytes.HEADER) return null
|
||||
|
||||
const magicNumber = dataView.getUint32(0, true)
|
||||
if (magicNumber !== ASCII.GLTF) return null
|
||||
|
||||
return {
|
||||
magicNumber,
|
||||
gltfFormatVersion: dataView.getUint32(4, true),
|
||||
totalLengthBytes: dataView.getUint32(8, true)
|
||||
}
|
||||
}
|
||||
|
||||
const parseChunkHeaderAtOffset = (
|
||||
dataView: DataView,
|
||||
offset: number
|
||||
): GltfChunkHeader | null => {
|
||||
if (offset + GltfSizeBytes.CHUNK_HEADER > dataView.byteLength) return null
|
||||
|
||||
return {
|
||||
chunkLengthBytes: dataView.getUint32(offset, true),
|
||||
chunkTypeIdentifier: dataView.getUint32(offset + 4, true)
|
||||
}
|
||||
}
|
||||
|
||||
const extractJsonChunk = (
|
||||
buffer: ArrayBuffer
|
||||
): { start: number; length: number } | null => {
|
||||
const dataView = new DataView(buffer)
|
||||
|
||||
const header = parseGltfBinaryHeader(dataView)
|
||||
if (!header) return null
|
||||
|
||||
const chunkOffset = GltfSizeBytes.HEADER
|
||||
const firstChunk = parseChunkHeaderAtOffset(dataView, chunkOffset)
|
||||
if (!firstChunk || !isJsonChunk(firstChunk)) return null
|
||||
|
||||
const jsonStart = chunkOffset + GltfSizeBytes.CHUNK_HEADER
|
||||
const isValid = isValidChunkRange(
|
||||
jsonStart,
|
||||
firstChunk.chunkLengthBytes,
|
||||
dataView.byteLength
|
||||
)
|
||||
if (!isValid) return null
|
||||
|
||||
return { start: jsonStart, length: firstChunk.chunkLengthBytes }
|
||||
}
|
||||
|
||||
const extractJsonChunkData = (buffer: ArrayBuffer): Uint8Array | null => {
|
||||
const chunkLocation = extractJsonChunk(buffer)
|
||||
if (!chunkLocation) return null
|
||||
|
||||
return new Uint8Array(buffer, chunkLocation.start, chunkLocation.length)
|
||||
}
|
||||
|
||||
const parseJson = (text: string): ReturnType<typeof JSON.parse> | null => {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const parseJsonBytes = (
|
||||
bytes: Uint8Array
|
||||
): ReturnType<typeof JSON.parse> | null => {
|
||||
const jsonString = byteArrayToString(bytes)
|
||||
return parseJson(jsonString)
|
||||
}
|
||||
|
||||
const parseMetadataValue = (
|
||||
value: string | object
|
||||
): ComfyWorkflowJSON | ComfyApiWorkflow | undefined => {
|
||||
if (typeof value !== 'string')
|
||||
return value as ComfyWorkflowJSON | ComfyApiWorkflow
|
||||
|
||||
const parsed = parseJson(value)
|
||||
if (!parsed) return undefined
|
||||
|
||||
return parsed as ComfyWorkflowJSON | ComfyApiWorkflow
|
||||
}
|
||||
|
||||
const extractComfyMetadata = (jsonData: GltfJsonData): ComfyMetadata => {
|
||||
const metadata: ComfyMetadata = {}
|
||||
|
||||
if (!jsonData?.asset?.extras) return metadata
|
||||
|
||||
const { extras } = jsonData.asset
|
||||
|
||||
if (extras.workflow) {
|
||||
const parsedValue = parseMetadataValue(extras.workflow)
|
||||
if (parsedValue) {
|
||||
metadata[ComfyMetadataTags.WORKFLOW.toLowerCase()] = parsedValue
|
||||
}
|
||||
}
|
||||
|
||||
if (extras.prompt) {
|
||||
const parsedValue = parseMetadataValue(extras.prompt)
|
||||
if (parsedValue) {
|
||||
metadata[ComfyMetadataTags.PROMPT.toLowerCase()] = parsedValue
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
const processGltfFileBuffer = (buffer: ArrayBuffer): ComfyMetadata => {
|
||||
const jsonChunk = extractJsonChunkData(buffer)
|
||||
if (!jsonChunk) return {}
|
||||
|
||||
const parsedJson = parseJsonBytes(jsonChunk)
|
||||
if (!parsedJson) return {}
|
||||
|
||||
return extractComfyMetadata(parsedJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ComfyUI metadata from a GLTF binary file (GLB)
|
||||
*/
|
||||
export function getGltfBinaryMetadata(file: File): Promise<ComfyMetadata> {
|
||||
return new Promise<ComfyMetadata>((resolve) => {
|
||||
if (!file) return Promise.resolve({})
|
||||
|
||||
const bytesToRead = Math.min(file.size, MAX_READ_BYTES)
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
if (!event.target?.result) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
|
||||
resolve(processGltfFileBuffer(event.target.result as ArrayBuffer))
|
||||
} catch {
|
||||
resolve({})
|
||||
}
|
||||
}
|
||||
reader.onerror = () => resolve({})
|
||||
reader.readAsArrayBuffer(file.slice(0, bytesToRead))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user