Add generation quality controls and bind them to the MiniMax workflow.
Expose aspect, CFG, FPS, sampler, and scheduler in Generation settings, persist them on clips and held jobs, and inject those values into the Comfy graph. Keep the image picker stills-only and allow renaming library clips. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -49,6 +49,10 @@ export interface Job {
|
||||
steps: number
|
||||
turbo: boolean
|
||||
seed: number
|
||||
cfg?: number
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
thumb?: Buffer
|
||||
imageName?: string
|
||||
imageSubfolder?: string
|
||||
|
||||
+37
-2
@@ -30,6 +30,10 @@ export interface LibraryClip {
|
||||
hideThumbnail: boolean
|
||||
createdAt: number
|
||||
comfyFilename?: string
|
||||
cfg?: number
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
}
|
||||
|
||||
export interface LibraryStill {
|
||||
@@ -71,6 +75,10 @@ export interface RetryDraft {
|
||||
sound: boolean
|
||||
hideInput?: boolean
|
||||
stillFilename?: string
|
||||
cfg?: number
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
}
|
||||
|
||||
interface Catalog {
|
||||
@@ -662,6 +670,10 @@ export async function saveRetryDraft(params: {
|
||||
hideThumbnail: boolean
|
||||
sound: boolean
|
||||
hideInput?: boolean
|
||||
cfg?: number
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
}) {
|
||||
return mutate(params.ownerKey, (catalog) => {
|
||||
const existing = params.id ? catalog.drafts.find(item => item.id === params.id) : undefined
|
||||
@@ -682,7 +694,11 @@ export async function saveRetryDraft(params: {
|
||||
duration: params.duration,
|
||||
hideThumbnail: params.hideThumbnail,
|
||||
sound: params.sound,
|
||||
hideInput: params.hideInput === true
|
||||
hideInput: params.hideInput === true,
|
||||
cfg: params.cfg,
|
||||
fps: params.fps,
|
||||
samplerName: params.samplerName,
|
||||
scheduler: params.scheduler
|
||||
}
|
||||
catalog.drafts = [draft, ...catalog.drafts.filter(item => item.id !== draft.id)].slice(0, 20)
|
||||
return draft
|
||||
@@ -743,6 +759,10 @@ export async function saveClip(params: {
|
||||
video: Buffer
|
||||
thumb?: Buffer | null
|
||||
comfyFilename?: string
|
||||
cfg?: number
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
}) {
|
||||
const catalog = readCatalog(params.ownerKey)
|
||||
const folder = catalog.folders.find(item => item.id === params.folderId) || catalog.folders[0]
|
||||
@@ -760,7 +780,11 @@ export async function saveClip(params: {
|
||||
seed: params.seed,
|
||||
hideThumbnail: params.hideThumbnail,
|
||||
createdAt: Date.now(),
|
||||
comfyFilename: params.comfyFilename
|
||||
comfyFilename: params.comfyFilename,
|
||||
cfg: params.cfg,
|
||||
fps: params.fps,
|
||||
samplerName: params.samplerName,
|
||||
scheduler: params.scheduler
|
||||
}
|
||||
mkdirSync(clipDir(params.ownerKey, clip.id), { recursive: true })
|
||||
writeFileSync(clipVideoPath(params.ownerKey, clip.id), params.video)
|
||||
@@ -813,6 +837,17 @@ export function moveClip(owner: string, id: string, folderId: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function renameClip(owner: string, id: string, name: string) {
|
||||
const trimmed = name.trim().slice(0, 80)
|
||||
if (!trimmed) throw createError({ statusCode: 400, statusMessage: 'A name is required' })
|
||||
return mutate(owner, (catalog) => {
|
||||
const clip = catalog.clips.find(item => item.id === id)
|
||||
if (!clip) throw createError({ statusCode: 404, statusMessage: 'Clip not found' })
|
||||
clip.name = trimmed
|
||||
return clip
|
||||
})
|
||||
}
|
||||
|
||||
export async function downloadComfyVideo(video: { filename: string; subfolder: string; type: string }) {
|
||||
const subfolders = [...new Set([video.subfolder, 'video', ''])]
|
||||
let lastStatus = 0
|
||||
|
||||
@@ -4,7 +4,7 @@ const LONG_EDGE = 1344
|
||||
export const ASPECT_PRESETS: Record<string, { width: number; height: number }> = {
|
||||
'16:9': { width: 1344, height: 768 },
|
||||
'9:16': { width: 768, height: 1344 },
|
||||
'1:1': { width: 1024, height: 1024 }
|
||||
'1:1': { width: 960, height: 960 }
|
||||
}
|
||||
|
||||
export function snap32(value: number) {
|
||||
@@ -85,16 +85,12 @@ function jpegSize(buf: Buffer) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function resolveOutputSize(aspect: string | undefined, image: Buffer, fallbackWidth?: number, fallbackHeight?: number) {
|
||||
export function resolveOutputSize(aspect: string | undefined, _image: Buffer, fallbackWidth?: number, fallbackHeight?: number) {
|
||||
if (aspect && ASPECT_PRESETS[aspect]) {
|
||||
return ASPECT_PRESETS[aspect]
|
||||
}
|
||||
const fromFields = Number(fallbackWidth) && Number(fallbackHeight)
|
||||
? { width: snap32(Number(fallbackWidth)), height: snap32(Number(fallbackHeight)) }
|
||||
: null
|
||||
const size = imageDimensions(image)
|
||||
if (size?.width && size?.height) {
|
||||
return autoResolution(size.width, size.height)
|
||||
if (Number(fallbackWidth) && Number(fallbackHeight)) {
|
||||
return { width: snap32(Number(fallbackWidth)), height: snap32(Number(fallbackHeight)) }
|
||||
}
|
||||
return fromFields || ASPECT_PRESETS['16:9']
|
||||
return ASPECT_PRESETS['16:9']
|
||||
}
|
||||
|
||||
@@ -72,7 +72,11 @@ export function watchComfyJob(job: Job): Promise<void> {
|
||||
hideInput: job.library.hideInput,
|
||||
video: buffer,
|
||||
thumb: job.library.thumb,
|
||||
comfyFilename: video.filename
|
||||
comfyFilename: video.filename,
|
||||
cfg: job.library.cfg,
|
||||
fps: job.library.fps,
|
||||
samplerName: job.library.samplerName,
|
||||
scheduler: job.library.scheduler
|
||||
})
|
||||
job.clipId = clip.id
|
||||
job.hideThumbnail = clip.hideThumbnail
|
||||
|
||||
@@ -10,6 +10,10 @@ export interface GenerateParams {
|
||||
seed: number
|
||||
turbo: boolean
|
||||
length: number
|
||||
cfg: number
|
||||
fps: number
|
||||
samplerName: string
|
||||
scheduler: string
|
||||
filenamePrefix?: string
|
||||
sound?: boolean
|
||||
}
|
||||
@@ -65,6 +69,25 @@ export function buildWorkflow(params: GenerateParams) {
|
||||
node.inputs.length = params.length
|
||||
}
|
||||
|
||||
if (node.class_type === 'CreateVideo' && 'fps' in node.inputs) {
|
||||
node.inputs.fps = params.fps
|
||||
}
|
||||
|
||||
if (node.class_type === 'KSamplerSelect' && 'sampler_name' in node.inputs) {
|
||||
node.inputs.sampler_name = params.samplerName
|
||||
}
|
||||
|
||||
if (node.class_type === 'BasicScheduler' && 'scheduler' in node.inputs) {
|
||||
node.inputs.scheduler = params.scheduler
|
||||
}
|
||||
|
||||
if ('cfg' in node.inputs && typeof node.inputs.cfg === 'number') {
|
||||
node.inputs.cfg = params.cfg
|
||||
}
|
||||
if ('guidance' in node.inputs && typeof node.inputs.guidance === 'number') {
|
||||
node.inputs.guidance = params.cfg
|
||||
}
|
||||
|
||||
if (SEED_CLASSES.has(node.class_type)) {
|
||||
if ('noise_seed' in node.inputs) node.inputs.noise_seed = params.seed
|
||||
if ('seed' in node.inputs) node.inputs.seed = params.seed
|
||||
@@ -91,6 +114,32 @@ export function buildWorkflow(params: GenerateParams) {
|
||||
}
|
||||
}
|
||||
|
||||
if (graph['128']) {
|
||||
graph['128'] = {
|
||||
class_type: 'ImageScale',
|
||||
inputs: {
|
||||
upscale_method: 'nearest-exact',
|
||||
width,
|
||||
height,
|
||||
crop: 'center',
|
||||
image: ['114', 0]
|
||||
},
|
||||
_meta: { title: 'Scaling image' }
|
||||
}
|
||||
}
|
||||
|
||||
graph['105:cfg'] = {
|
||||
class_type: 'FluxGuidance',
|
||||
inputs: {
|
||||
guidance: params.cfg,
|
||||
conditioning: ['105:104', 0]
|
||||
},
|
||||
_meta: { title: 'Guidance scale' }
|
||||
}
|
||||
if (graph['105:16']?.inputs) {
|
||||
graph['105:16'].inputs.conditioning = ['105:cfg', 0]
|
||||
}
|
||||
|
||||
for (const [, node] of entriesOf(graph, 'ComfySwitchNode')) {
|
||||
if (!/step/i.test(titleOf(node))) continue
|
||||
const qualityId = linkId(node.inputs.on_false)
|
||||
@@ -127,6 +176,7 @@ const LABEL_OVERRIDES: Record<string, string> = {
|
||||
'105:10': 'Decoding video frames',
|
||||
'105:14': 'Sampling',
|
||||
'105:16': 'Preparing guider',
|
||||
'105:cfg': 'Applying guidance',
|
||||
'105:17': 'Selecting sampler',
|
||||
'105:23': 'Decoding audio',
|
||||
'105:24': 'Loading audio VAE',
|
||||
|
||||
Reference in New Issue
Block a user