Files
vfxreview/scripts/diagnose-thumbnails.mjs
twotalesanimation 1fad6a739d
Deploy / deploy (push) Successful in 9m16s
fix(worker): drain ffmpeg's pipes so thumbnail generation stops deadlocking
ffmpeg was started with stdout and stderr redirected and neither pipe ever
read. Windows gives an anonymous pipe a ~4 KB buffer, which ffmpeg's banner
and stream summary exceed on their own, so it blocked forever on the write,
never exited, and the two-minute cap fired:

    TaskCanceledException at Process.WaitForExitAsync
    Preview complete for job ... in 221s (mov + mp4)

The exception was swallowed as "non-fatal", so every preview silently
produced no thumbnail while paying 120s for the privilege. Nothing
downstream was wrong — there was simply never a thumbnail to promote.

Both pipes are now drained concurrently before the wait. A timeout kills the
process tree rather than leaking an ffmpeg per job, and attempts report a
three-way outcome so a hung or unstartable ffmpeg is not retried — without
that, the slate fallback would have doubled the wasted time to four minutes.

Failures are no longer silent: an unset or missing ffmpegPath, the seek that
was chosen, and ffmpeg's own exit code, command and stderr are all logged.

finalizePreview likewise records every promotion outcome on the export
timeline, not just success. Silence there was indistinguishable from a broken
worker, a project whose flag never saved, and the feature working as
configured — which is what made this take so long to find.

Adds scripts/diagnose-thumbnails.mjs, which walks the chain (ffmpeg →
Export.thumbnailKey → project flag → Shot.generatedThumbnailUrl → what the UI
resolves) and names the first broken link.

Worker 1.5.1 -> 1.5.2; republish required for the fix to take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:29 +02:00

91 lines
3.6 KiB
JavaScript

/**
* Why is a shot not showing a render-derived thumbnail?
*
* Walks the chain in order and reports the first broken link:
*
* ffmpeg on the worker → Export.thumbnailKey → project flag →
* Shot.generatedThumbnailUrl → what the UI resolves
*
* Usage: node scripts/diagnose-thumbnails.mjs [PROJECT_CODE]
*/
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
const projectCode = process.argv[2]?.toUpperCase();
const projects = await db.project.findMany({
where: projectCode ? { code: projectCode } : {},
select: { id: true, code: true, name: true, pipelineConfig: true },
orderBy: { code: "asc" },
});
if (projects.length === 0) {
console.log(projectCode ? `No project with code ${projectCode}.` : "No projects.");
process.exit(0);
}
for (const p of projects) {
const cfg = (p.pipelineConfig ?? {});
const flag = cfg.autoShotThumbnail;
const on = flag === true;
console.log(`\n━━ ${p.code} — ${p.name}`);
console.log(` auto-promote flag : ${JSON.stringify(flag)} ${on ? "✔ on" : "✘ OFF"}`);
if (flag === undefined) {
console.log(` ⚠ key absent from pipelineConfig. Either the setting was never saved,`);
console.log(` or the server that saved it predates the boolean fix in actions/projects.ts.`);
console.log(` Stored keys: ${JSON.stringify(Object.keys(cfg))}`);
}
const exports = await db.export.findMany({
where: { shot: { projectId: p.id } },
orderBy: { createdAt: "desc" },
take: 10,
select: {
versionString: true, status: true, createdAt: true,
thumbnailKey: true, skipPreview: true,
shot: {
select: {
shotCode: true, thumbnailUrl: true,
generatedThumbnailUrl: true, generatedThumbnailFrom: true,
},
},
},
});
if (exports.length === 0) {
console.log(" (no exports yet)");
continue;
}
console.log(` last ${exports.length} exports:`);
for (const e of exports) {
const s = e.shot;
const displayed = s.thumbnailUrl || s.generatedThumbnailUrl || null;
const source = s.thumbnailUrl ? "reference" : s.generatedThumbnailUrl ? "render" : "NOTHING";
console.log(
`\n ${s.shotCode} ${e.versionString} [${e.status}]${e.skipPreview ? " skipPreview" : ""}`
);
console.log(` export.thumbnailKey : ${e.thumbnailKey ?? "✘ NULL — worker produced no thumbnail (ffmpeg?)"}`);
console.log(` shot.thumbnailUrl : ${s.thumbnailUrl ?? "(none)"}`);
console.log(` shot.generatedThumbnail : ${s.generatedThumbnailUrl ?? "(none)"}${s.generatedThumbnailFrom ? ` [from ${s.generatedThumbnailFrom}]` : ""}`);
console.log(` → UI shows : ${displayed ?? "nothing"} (${source})`);
if (!e.thumbnailKey) {
console.log(` ⇒ Broken at the WORKER. Check ffmpegPath in config.json on the`);
console.log(` machine that ran it, and its log for "no thumbnail".`);
} else if (!on) {
console.log(` ⇒ Thumbnail exists but the project flag is off, so it was not promoted.`);
console.log(` Turn on "Use render frames as shot thumbnails", or use the`);
console.log(` per-export "Use frame" button on the shot's Exports tab.`);
} else if (!s.generatedThumbnailUrl) {
console.log(` ⇒ Thumbnail exists and the flag is on, but nothing was promoted.`);
console.log(` This export probably finalized BEFORE the flag was turned on —`);
console.log(` promotion happens at finalize, it is not retroactive. Re-render,`);
console.log(` or press "Use frame" on the Exports tab.`);
}
}
}
await db.$disconnect();