Tighten needsFit to exact canvas size, guard against empty renders
needsFit only compared aspect ratio, so a same-aspect-but-larger still (e.g. a 4032x2268 phone photo against a 1344x768 canvas) would skip fitting and hand MiniMaxH3AddGuide a still that does not match the job's own latent size. Require an exact width+height match instead; anything else (including unreadable dimensions) still fits. Also guard the video save path: an empty Comfy download, an empty stitched assembly, or a stitched result with no readable duration now fails the job with a clear error instead of silently saving/attaching a broken zero-length clip. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,12 +12,12 @@ export async function fitStill(data: Buffer, width: number, height: number) {
|
||||
return readFileSync(output)
|
||||
} finally { rmSync(dir,{recursive:true,force:true}) }
|
||||
}
|
||||
/** Only scale/pad when the still's aspect truly differs from the job canvas.
|
||||
* A matching frame (typical extend handoff) must reach Comfy byte-identical. */
|
||||
/** Only skip the scale+pad when the still already matches the job canvas pixel-for-pixel.
|
||||
* A same-aspect-but-larger still (e.g. a 4032x2268 phone photo against a 1344x768 canvas)
|
||||
* still needs fitting: MiniMaxH3AddGuide expects an image sized to the job's own latent,
|
||||
* not merely the same aspect ratio. Unreadable dimensions always fit (safe default). */
|
||||
export function needsFit(data: Buffer, width: number, height: number) {
|
||||
const size = imageDimensions(data)
|
||||
if (!size?.width || !size?.height) return true
|
||||
if (size.width === width && size.height === height) return false
|
||||
const target = width / height, actual = size.width / size.height
|
||||
return Math.abs(target - actual) > 0.01
|
||||
return !(size.width === width && size.height === height)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { watchProgress } from './progress';
|
||||
import { fitStill, needsFit } from './media';
|
||||
import { resolveRequestSize } from './size';
|
||||
import { queueSeeds } from '~/shared/studio2/seed.mjs';
|
||||
import { readFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import template from '../../assets/studio2_minimax_native.json';
|
||||
import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize, sampleKleinSource } from '~/shared/studio2/graphs.mjs';
|
||||
@@ -19,7 +19,7 @@ import { buildImageV2Workflow, resolveKreaGenerateAssets } from '../imageWorkflo
|
||||
import { resolveGraphLoraNames, applyUserLoraToGraph, ensureComfyLoraNames } from '../loras';
|
||||
import { getClip, getStill, stillPath, clipVideoPath, saveStill, saveClip, downloadComfyImage, downloadComfyVideo, attachStudio2Metadata } from '../library';
|
||||
import { imageDimensions } from '../resolution';
|
||||
import { stitchExtension } from '../ffmpeg';
|
||||
import { stitchExtension, probeDuration } from '../ffmpeg';
|
||||
import { videoSourcePaths } from '../videoSources';
|
||||
import { resolveExtensionHandoffFrame, persistClipAnchorFrame } from '../extensionFrame';
|
||||
import { acquireSharedGpu, sharedGpuHeaders } from '../sharedGpu';
|
||||
@@ -202,10 +202,21 @@ async function run(r: any, job: Job) {
|
||||
let asset: any = r.savedAssetId ? (video ? getClip(r.owner, r.savedAssetId) : getStill(r.owner, r.savedAssetId)) : null;
|
||||
if (!asset && video) {
|
||||
const data = await downloadComfyVideo(file);
|
||||
if (!data.length || data.length < 64)
|
||||
throw new Error('Comfy wrote an empty video file (<64 bytes); the job did not render. Nothing was saved.');
|
||||
const sourceSegments = q.startClipId ? videoSourcePaths(clipVideoPath(r.owner, q.startClipId)) : [];
|
||||
const tmp = join(studio2Root(), r.id);
|
||||
mkdirSync(tmp, { recursive: true });
|
||||
const assembled = q.startClipId ? await stitchExtension({ part1Path: clipVideoPath(r.owner, q.startClipId), part2: data, tmpDir: tmp, sourcePaths: sourceSegments }) : data;
|
||||
if (!assembled.length || assembled.length < 64)
|
||||
throw new Error('The assembled video file is empty (<64 bytes); the job did not render. Nothing was saved.');
|
||||
const checkPath = join(tmp, 'duration-check.mp4');
|
||||
writeFileSync(checkPath, assembled);
|
||||
let duration = 0;
|
||||
try { duration = await probeDuration(checkPath); }
|
||||
finally { try { unlinkSync(checkPath); } catch { /* ignore */ } }
|
||||
if (!Number.isFinite(duration) || duration <= 0)
|
||||
throw new Error('The rendered video has no readable duration; the job did not render. Nothing was saved.');
|
||||
asset = await saveClip({ ownerKey: r.owner, folderId: q.folderId, prompt: q.compiledPrompt, ...s, aspect: s.aspect || 'auto', hideThumbnail: false, video: assembled, originalSegment: data, sourceSegments, fps: s.fps || 24, sound: true, familyId: r.familyId, parentClipId: q.startClipId || undefined, chainIndex: r.index, comfyFilename: file.filename });
|
||||
}
|
||||
else if (!asset) {
|
||||
|
||||
Reference in New Issue
Block a user