mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-28 02:34:10 +00:00
## Summary Removes **254** `@ts-expect-error` suppressions through proper type fixes rather than type assertions. ## Key Changes ### Type System Improvements - Add `globalDefs` and `groupNodes` types to `ComfyAppWindowExtension` - Extract interfaces for group node handling (`GroupNodeHandler`, `InnerNodeOutput`, etc.) - Add `getHandler()` helper to consolidate GROUP symbol access pattern ### Files Fixed - **pnginfo.ts**: 39 suppressions removed via proper typing of workflow/prompt data - **app.ts**: 39 suppressions removed via interface extraction and type narrowing - **Tier 1 files**: 17 suppressions removed (maskeditor, imageDrawer, groupNode, etc.) - **groupNode.ts**: Major refactoring with proper interface organization ## Approach Following established constraints: - No `any` types - No `as unknown as T` casts (except legacy API boundaries) - Priority: Fix actual types > Type narrowing > Targeted suppressions as last resort - Prefix unused callback parameters with underscore - Extract repeated inline types into named interfaces ## Validation - ✅ `pnpm typecheck` passes - ✅ `pnpm lint` passes - ✅ `pnpm knip` passes ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7884-Chore-TypeScript-cleanup-remove-254-ts-expect-error-suppressions-2e26d73d3650812e9b48da203ce1d296) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
544 lines
16 KiB
TypeScript
544 lines
16 KiB
TypeScript
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
|
|
|
import { api } from './api'
|
|
import { getFromAvifFile } from './metadata/avif'
|
|
import { getFromFlacFile } from './metadata/flac'
|
|
import { getFromPngFile } from './metadata/png'
|
|
|
|
// Original functions left in for backwards compatibility
|
|
export function getPngMetadata(file: File): Promise<Record<string, string>> {
|
|
return getFromPngFile(file)
|
|
}
|
|
|
|
export function getFlacMetadata(file: File): Promise<Record<string, string>> {
|
|
return getFromFlacFile(file)
|
|
}
|
|
|
|
export function getAvifMetadata(file: File): Promise<Record<string, string>> {
|
|
return getFromAvifFile(file)
|
|
}
|
|
|
|
function parseExifData(exifData: Uint8Array) {
|
|
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
|
|
const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === 'II'
|
|
|
|
// Function to read 16-bit and 32-bit integers from binary data
|
|
function readInt(
|
|
offset: number,
|
|
isLittleEndian: boolean,
|
|
length: 2 | 4
|
|
): number {
|
|
let arr = exifData.slice(offset, offset + length)
|
|
if (length === 2) {
|
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
|
|
0,
|
|
isLittleEndian
|
|
)
|
|
} else if (length === 4) {
|
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(
|
|
0,
|
|
isLittleEndian
|
|
)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Read the offset to the first IFD (Image File Directory)
|
|
const ifdOffset = readInt(4, isLittleEndian, 4)
|
|
|
|
function parseIFD(offset: number): Record<number, string | undefined> {
|
|
const numEntries = readInt(offset, isLittleEndian, 2)
|
|
const result: Record<number, string | undefined> = {}
|
|
|
|
for (let i = 0; i < numEntries; i++) {
|
|
const entryOffset = offset + 2 + i * 12
|
|
const tag = readInt(entryOffset, isLittleEndian, 2)
|
|
const type = readInt(entryOffset + 2, isLittleEndian, 2)
|
|
const numValues = readInt(entryOffset + 4, isLittleEndian, 4)
|
|
const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4)
|
|
|
|
// Read the value(s) based on the data type
|
|
let value
|
|
if (type === 2) {
|
|
// ASCII string
|
|
value = new TextDecoder('utf-8').decode(
|
|
exifData.subarray(valueOffset, valueOffset + numValues - 1)
|
|
)
|
|
}
|
|
|
|
result[tag] = value
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Parse the first IFD
|
|
const ifdData = parseIFD(ifdOffset)
|
|
return ifdData
|
|
}
|
|
|
|
export function getWebpMetadata(file: File) {
|
|
return new Promise<Record<string, string>>((r) => {
|
|
const reader = new FileReader()
|
|
reader.onload = (event) => {
|
|
const webp = new Uint8Array(event.target?.result as ArrayBuffer)
|
|
const dataView = new DataView(webp.buffer)
|
|
|
|
// Check that the WEBP signature is present
|
|
if (
|
|
dataView.getUint32(0) !== 0x52494646 ||
|
|
dataView.getUint32(8) !== 0x57454250
|
|
) {
|
|
console.error('Not a valid WEBP file')
|
|
r({})
|
|
return
|
|
}
|
|
|
|
// Start searching for chunks after the WEBP signature
|
|
let offset = 12
|
|
const txt_chunks: Record<string, string> = {}
|
|
// Loop through the chunks in the WEBP file
|
|
while (offset < webp.length) {
|
|
const chunk_length = dataView.getUint32(offset + 4, true)
|
|
const chunk_type = String.fromCharCode(
|
|
...webp.slice(offset, offset + 4)
|
|
)
|
|
if (chunk_type === 'EXIF') {
|
|
if (
|
|
String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) ==
|
|
'Exif\0\0'
|
|
) {
|
|
offset += 6
|
|
}
|
|
let data = parseExifData(
|
|
webp.slice(offset + 8, offset + 8 + chunk_length)
|
|
)
|
|
for (const key in data) {
|
|
const value = data[Number(key)]
|
|
if (typeof value === 'string') {
|
|
const index = value.indexOf(':')
|
|
txt_chunks[value.slice(0, index)] = value.slice(index + 1)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
offset += 8 + chunk_length
|
|
}
|
|
|
|
r(txt_chunks)
|
|
}
|
|
|
|
reader.readAsArrayBuffer(file)
|
|
})
|
|
}
|
|
|
|
export function getLatentMetadata(file: File): Promise<Record<string, string>> {
|
|
return new Promise((r) => {
|
|
const reader = new FileReader()
|
|
reader.onload = (event) => {
|
|
const safetensorsData = new Uint8Array(
|
|
event.target?.result as ArrayBuffer
|
|
)
|
|
const dataView = new DataView(safetensorsData.buffer)
|
|
let header_size = dataView.getUint32(0, true)
|
|
let offset = 8
|
|
let header = JSON.parse(
|
|
new TextDecoder().decode(
|
|
safetensorsData.slice(offset, offset + header_size)
|
|
)
|
|
)
|
|
r(header.__metadata__)
|
|
}
|
|
|
|
var slice = file.slice(0, 1024 * 1024 * 4)
|
|
reader.readAsArrayBuffer(slice)
|
|
})
|
|
}
|
|
|
|
interface NodeConnection {
|
|
node: LGraphNode
|
|
index: number
|
|
}
|
|
|
|
interface LoraEntry {
|
|
name: string
|
|
weight: number
|
|
}
|
|
|
|
export async function importA1111(
|
|
graph: LGraph,
|
|
parameters: string
|
|
): Promise<void> {
|
|
const p = parameters.lastIndexOf('\nSteps:')
|
|
if (p > -1) {
|
|
const embeddings = await api.getEmbeddings()
|
|
const matchResult = parameters
|
|
.substr(p)
|
|
.split('\n')[1]
|
|
.match(
|
|
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', 'g')
|
|
)
|
|
if (!matchResult) return
|
|
|
|
const opts: Record<string, string> = matchResult.reduce(
|
|
(acc: Record<string, string>, n: string) => {
|
|
const s = n.split(':')
|
|
if (s[1].endsWith(',')) {
|
|
s[1] = s[1].substr(0, s[1].length - 1)
|
|
}
|
|
acc[s[0].trim().toLowerCase()] = s[1].trim()
|
|
return acc
|
|
},
|
|
{}
|
|
)
|
|
const p2 = parameters.lastIndexOf('\nNegative prompt:', p)
|
|
if (p2 > -1) {
|
|
let positive = parameters.substr(0, p2).trim()
|
|
let negative = parameters.substring(p2 + 18, p).trim()
|
|
|
|
const ckptNode = LiteGraph.createNode('CheckpointLoaderSimple')
|
|
const clipSkipNode = LiteGraph.createNode('CLIPSetLastLayer')
|
|
const positiveNode = LiteGraph.createNode('CLIPTextEncode')
|
|
const negativeNode = LiteGraph.createNode('CLIPTextEncode')
|
|
const samplerNode = LiteGraph.createNode('KSampler')
|
|
const imageNode = LiteGraph.createNode('EmptyLatentImage')
|
|
const vaeNode = LiteGraph.createNode('VAEDecode')
|
|
const saveNode = LiteGraph.createNode('SaveImage')
|
|
|
|
if (
|
|
!ckptNode ||
|
|
!clipSkipNode ||
|
|
!positiveNode ||
|
|
!negativeNode ||
|
|
!samplerNode ||
|
|
!imageNode ||
|
|
!vaeNode ||
|
|
!saveNode
|
|
) {
|
|
console.error('Failed to create required nodes for A1111 import')
|
|
return
|
|
}
|
|
|
|
let hrSamplerNode: LGraphNode | null = null
|
|
let hrSteps: string | null = null
|
|
|
|
const ceil64 = (v: number) => Math.ceil(v / 64) * 64
|
|
|
|
function getWidget(
|
|
node: LGraphNode | null,
|
|
name: string
|
|
): IBaseWidget | undefined {
|
|
return node?.widgets?.find((w) => w.name === name)
|
|
}
|
|
|
|
function setWidgetValue(
|
|
node: LGraphNode | null,
|
|
name: string,
|
|
value: string | number,
|
|
isOptionPrefix?: boolean
|
|
): void {
|
|
const w = getWidget(node, name)
|
|
if (!w) return
|
|
|
|
if (isOptionPrefix) {
|
|
const values = w.options.values as string[] | undefined
|
|
const o = values?.find((v) => v.startsWith(String(value)))
|
|
if (o) {
|
|
w.value = o
|
|
} else {
|
|
console.warn(`Unknown value '${value}' for widget '${name}'`, node)
|
|
w.value = value
|
|
}
|
|
} else {
|
|
w.value = value
|
|
}
|
|
}
|
|
|
|
function createLoraNodes(
|
|
clipNode: LGraphNode,
|
|
text: string,
|
|
prevClip: NodeConnection,
|
|
prevModel: NodeConnection,
|
|
targetSamplerNode: LGraphNode
|
|
): { text: string; prevModel: NodeConnection; prevClip: NodeConnection } {
|
|
const loras: LoraEntry[] = []
|
|
text = text.replace(/<lora:([^:]+:[^>]+)>/g, (_m, c: string) => {
|
|
const s = c.split(':')
|
|
const weight = parseFloat(s[1])
|
|
if (isNaN(weight)) {
|
|
console.warn('Invalid LORA', _m)
|
|
} else {
|
|
loras.push({ name: s[0], weight })
|
|
}
|
|
return ''
|
|
})
|
|
|
|
for (const l of loras) {
|
|
const loraNode = LiteGraph.createNode('LoraLoader')
|
|
if (!loraNode) continue
|
|
graph.add(loraNode)
|
|
setWidgetValue(loraNode, 'lora_name', l.name, true)
|
|
setWidgetValue(loraNode, 'strength_model', l.weight)
|
|
setWidgetValue(loraNode, 'strength_clip', l.weight)
|
|
prevModel.node.connect(prevModel.index, loraNode, 0)
|
|
prevClip.node.connect(prevClip.index, loraNode, 1)
|
|
prevModel = { node: loraNode, index: 0 }
|
|
prevClip = { node: loraNode, index: 1 }
|
|
}
|
|
|
|
prevClip.node.connect(1, clipNode, 0)
|
|
prevModel.node.connect(0, targetSamplerNode, 0)
|
|
if (hrSamplerNode) {
|
|
prevModel.node.connect(0, hrSamplerNode, 0)
|
|
}
|
|
|
|
return { text, prevModel, prevClip }
|
|
}
|
|
|
|
function replaceEmbeddings(text: string): string {
|
|
if (!embeddings.length) return text
|
|
return text.replaceAll(
|
|
new RegExp(
|
|
'\\b(' +
|
|
embeddings
|
|
.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
.join('\\b|\\b') +
|
|
')\\b',
|
|
'ig'
|
|
),
|
|
'embedding:$1'
|
|
)
|
|
}
|
|
|
|
function popOpt(name: string): string | undefined {
|
|
const v = opts[name]
|
|
delete opts[name]
|
|
return v
|
|
}
|
|
|
|
graph.clear()
|
|
graph.add(ckptNode)
|
|
graph.add(clipSkipNode)
|
|
graph.add(positiveNode)
|
|
graph.add(negativeNode)
|
|
graph.add(samplerNode)
|
|
graph.add(imageNode)
|
|
graph.add(vaeNode)
|
|
graph.add(saveNode)
|
|
|
|
ckptNode.connect(1, clipSkipNode, 0)
|
|
clipSkipNode.connect(0, positiveNode, 0)
|
|
clipSkipNode.connect(0, negativeNode, 0)
|
|
ckptNode.connect(0, samplerNode, 0)
|
|
positiveNode.connect(0, samplerNode, 1)
|
|
negativeNode.connect(0, samplerNode, 2)
|
|
imageNode.connect(0, samplerNode, 3)
|
|
vaeNode.connect(0, saveNode, 0)
|
|
samplerNode.connect(0, vaeNode, 0)
|
|
ckptNode.connect(2, vaeNode, 1)
|
|
|
|
const handlers: Record<string, (v: string) => void> = {
|
|
model(v: string) {
|
|
setWidgetValue(ckptNode, 'ckpt_name', v, true)
|
|
},
|
|
vae() {},
|
|
'cfg scale'(v: string) {
|
|
setWidgetValue(samplerNode, 'cfg', +v)
|
|
},
|
|
'clip skip'(v: string) {
|
|
setWidgetValue(clipSkipNode, 'stop_at_clip_layer', -Number(v))
|
|
},
|
|
sampler(v: string) {
|
|
let name = v.toLowerCase().replace('++', 'pp').replaceAll(' ', '_')
|
|
if (name.includes('karras')) {
|
|
name = name.replace('karras', '').replace(/_+$/, '')
|
|
setWidgetValue(samplerNode, 'scheduler', 'karras')
|
|
} else {
|
|
setWidgetValue(samplerNode, 'scheduler', 'normal')
|
|
}
|
|
const w = getWidget(samplerNode, 'sampler_name')
|
|
const values = w?.options.values as string[] | undefined
|
|
const o = values?.find((v) => v === name || v === 'sample_' + name)
|
|
if (o) {
|
|
setWidgetValue(samplerNode, 'sampler_name', o)
|
|
}
|
|
},
|
|
size(v: string) {
|
|
const wxh = v.split('x')
|
|
const w = ceil64(+wxh[0])
|
|
const h = ceil64(+wxh[1])
|
|
const hrUp = popOpt('hires upscale')
|
|
const hrSz = popOpt('hires resize')
|
|
hrSteps = popOpt('hires steps') ?? null
|
|
let hrMethod = popOpt('hires upscaler')
|
|
|
|
setWidgetValue(imageNode, 'width', w)
|
|
setWidgetValue(imageNode, 'height', h)
|
|
|
|
if (hrUp || hrSz) {
|
|
let uw: number, uh: number
|
|
if (hrUp) {
|
|
uw = w * Number(hrUp)
|
|
uh = h * Number(hrUp)
|
|
} else if (hrSz) {
|
|
const s = hrSz.split('x')
|
|
uw = +s[0]
|
|
uh = +s[1]
|
|
} else {
|
|
return
|
|
}
|
|
|
|
let upscaleNode: LGraphNode | null
|
|
let latentNode: LGraphNode | null
|
|
|
|
if (hrMethod?.startsWith('Latent')) {
|
|
latentNode = upscaleNode = LiteGraph.createNode('LatentUpscale')
|
|
if (!upscaleNode) return
|
|
graph.add(upscaleNode)
|
|
samplerNode.connect(0, upscaleNode, 0)
|
|
|
|
switch (hrMethod) {
|
|
case 'Latent (nearest-exact)':
|
|
hrMethod = 'nearest-exact'
|
|
break
|
|
}
|
|
setWidgetValue(upscaleNode, 'upscale_method', hrMethod, true)
|
|
} else {
|
|
const decode = LiteGraph.createNode('VAEDecodeTiled')
|
|
if (!decode) return
|
|
graph.add(decode)
|
|
samplerNode.connect(0, decode, 0)
|
|
ckptNode.connect(2, decode, 1)
|
|
|
|
const upscaleLoaderNode =
|
|
LiteGraph.createNode('UpscaleModelLoader')
|
|
if (!upscaleLoaderNode) return
|
|
graph.add(upscaleLoaderNode)
|
|
setWidgetValue(
|
|
upscaleLoaderNode,
|
|
'model_name',
|
|
hrMethod ?? '',
|
|
true
|
|
)
|
|
|
|
const modelUpscaleNode = LiteGraph.createNode(
|
|
'ImageUpscaleWithModel'
|
|
)
|
|
if (!modelUpscaleNode) return
|
|
graph.add(modelUpscaleNode)
|
|
decode.connect(0, modelUpscaleNode, 1)
|
|
upscaleLoaderNode.connect(0, modelUpscaleNode, 0)
|
|
|
|
upscaleNode = LiteGraph.createNode('ImageScale')
|
|
if (!upscaleNode) return
|
|
graph.add(upscaleNode)
|
|
modelUpscaleNode.connect(0, upscaleNode, 0)
|
|
|
|
const vaeEncodeNode = LiteGraph.createNode('VAEEncodeTiled')
|
|
if (!vaeEncodeNode) return
|
|
latentNode = vaeEncodeNode
|
|
graph.add(vaeEncodeNode)
|
|
upscaleNode.connect(0, vaeEncodeNode, 0)
|
|
ckptNode.connect(2, vaeEncodeNode, 1)
|
|
}
|
|
|
|
setWidgetValue(upscaleNode, 'width', ceil64(uw))
|
|
setWidgetValue(upscaleNode, 'height', ceil64(uh))
|
|
|
|
hrSamplerNode = LiteGraph.createNode('KSampler')
|
|
if (!hrSamplerNode || !latentNode) return
|
|
graph.add(hrSamplerNode)
|
|
ckptNode.connect(0, hrSamplerNode, 0)
|
|
positiveNode.connect(0, hrSamplerNode, 1)
|
|
negativeNode.connect(0, hrSamplerNode, 2)
|
|
latentNode.connect(0, hrSamplerNode, 3)
|
|
hrSamplerNode.connect(0, vaeNode, 0)
|
|
}
|
|
},
|
|
steps(v: string) {
|
|
setWidgetValue(samplerNode, 'steps', +v)
|
|
},
|
|
seed(v: string) {
|
|
setWidgetValue(samplerNode, 'seed', +v)
|
|
}
|
|
}
|
|
|
|
for (const opt in opts) {
|
|
const handler = handlers[opt]
|
|
if (handler) {
|
|
const value = popOpt(opt)
|
|
if (value !== undefined) handler(value)
|
|
}
|
|
}
|
|
|
|
if (hrSamplerNode) {
|
|
setWidgetValue(
|
|
hrSamplerNode,
|
|
'steps',
|
|
hrSteps
|
|
? +hrSteps
|
|
: (getWidget(samplerNode, 'steps')?.value as number)
|
|
)
|
|
setWidgetValue(
|
|
hrSamplerNode,
|
|
'cfg',
|
|
getWidget(samplerNode, 'cfg')?.value as number
|
|
)
|
|
setWidgetValue(
|
|
hrSamplerNode,
|
|
'scheduler',
|
|
getWidget(samplerNode, 'scheduler')?.value as string
|
|
)
|
|
setWidgetValue(
|
|
hrSamplerNode,
|
|
'sampler_name',
|
|
getWidget(samplerNode, 'sampler_name')?.value as string
|
|
)
|
|
setWidgetValue(
|
|
hrSamplerNode,
|
|
'denoise',
|
|
+(popOpt('denoising strength') ?? '1')
|
|
)
|
|
}
|
|
|
|
let n = createLoraNodes(
|
|
positiveNode,
|
|
positive,
|
|
{ node: clipSkipNode, index: 0 },
|
|
{ node: ckptNode, index: 0 },
|
|
samplerNode
|
|
)
|
|
positive = n.text
|
|
n = createLoraNodes(
|
|
negativeNode,
|
|
negative,
|
|
n.prevClip,
|
|
n.prevModel,
|
|
samplerNode
|
|
)
|
|
negative = n.text
|
|
|
|
setWidgetValue(positiveNode, 'text', replaceEmbeddings(positive))
|
|
setWidgetValue(negativeNode, 'text', replaceEmbeddings(negative))
|
|
|
|
graph.arrange()
|
|
|
|
for (const opt of [
|
|
'model hash',
|
|
'ensd',
|
|
'version',
|
|
'vae hash',
|
|
'ti hashes',
|
|
'lora hashes',
|
|
'hashes'
|
|
]) {
|
|
delete opts[opt]
|
|
}
|
|
|
|
console.warn('Unhandled parameters:', opts)
|
|
}
|
|
}
|
|
}
|