From d35b9e2c3153c8a809e5ea4f33d4321486a59bcf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 25 Oct 2026 18:32:26 +0100 Subject: [PATCH] A sample's file and window pair by its own index, not by sorting the file names as text motion_100.bvh sorted before motion_11.bvh, so past 100 samples a clip was exported over another sample's window. The index is read from the file name and orders the samples and names the clips. Co-Authored-By: Claude Opus 5.5 --- scripts/test-vili-daemon.mjs | 43 ++++++++++++++++++++++++++++++++++++ scripts/vili-daemon.mjs | 12 ++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/scripts/test-vili-daemon.mjs b/scripts/test-vili-daemon.mjs index 7c9af59..dc3ffaf 100644 --- a/scripts/test-vili-daemon.mjs +++ b/scripts/test-vili-daemon.mjs @@ -1504,6 +1504,49 @@ test("constraints given twice are refused before anything is written or spawned" } }); +test("past 100 samples each clip is exported over the window of its own sample", async () => { + const daemon = await startDaemon(); + try { + const answer = await generate(daemon, exportRequest({ clipName: "m", loop: true, numSamples: 101, exportTimeoutMs: 60000 })); + assert.equal(answer.status, 200); + const starts = exportStarts(daemon); + assert.equal(answer.body.export.clips.length, 101); + answer.body.export.clips.forEach((clip, i) => { + const sample = Number(/motion_(\d+)\.fbx$/.exec(clip.file)[1]); + const argv = starts[i].argv.slice(starts[i].argv.indexOf("--") + 1); + assert.equal(argv[2], `m_${String(sample).padStart(2, "0")}`, `${clip.file} is named for its sample`); + assert.deepEqual(argv.slice(3), ["--frames", String(4 + sample), String(224 + sample)], `${clip.file} gets its own sample's window`); + }); + assert.ok(answer.body.export.clips.some((clip) => clip.file.endsWith("motion_100.fbx"))); + } finally { + await daemon.stop(); + } +}); + +test("constraints given twice are refused before anything is written or spawned", async () => { + const daemon = await startDaemon(); + try { + const inline = [{ type: "root2d" }]; + for (const bad of [ + { constraints: inline, constraints_path: "/canary/a.json" }, + { constraints: inline, constraintsPath: "/canary/a.json" }, + { constraints: "/canary/a.json", constraints_path: "/canary/b.json" }, + { constraints_path: "/canary/a.json", constraintsPath: "/canary/b.json" }, + ]) { + const answer = await generate(daemon, exportRequest(bad)); + assert.equal(answer.status, 400, JSON.stringify(bad)); + assert.deepEqual(answer.body, { ok: false, error: "Invalid constraints request." }); + assert.ok(!answer.raw.includes("canary"), "the refusal repeated the input"); + } + assert.equal(existsSync(path.join(daemon.stateRoot, "artifacts")), false, "no artifact directory was made"); + assert.deepEqual(jobFiles(daemon), []); + assert.deepEqual(daemon.spawns(), [], "no worker was started"); + assert.equal((await generate(daemon, exportRequest({ constraints_path: "/some/where.json" }))).status, 200, "one input is fine"); + } finally { + await daemon.stop(); + } +}); + test("loop is a boolean and needs fbx, refused before anything is written or spawned", async () => { const daemon = await startDaemon(); try { diff --git a/scripts/vili-daemon.mjs b/scripts/vili-daemon.mjs index 67fa254..edb83d9 100644 --- a/scripts/vili-daemon.mjs +++ b/scripts/vili-daemon.mjs @@ -650,8 +650,8 @@ async function sweepWork() { // Every BVH the worker listed becomes an FBX, or the job fails. A listed BVH must be a regular file inside the job's // artifact directory, and that is decided when its export begins (exportClip), never earlier. Sample N of several is -// _NN. With loops (the worker's answer, an array when the request asked for a loop, else undefined), sample k -// is exported over loops[k], and a job whose answer has no usable window for a sample fails. +// _NN, N being its own index. With loops (the worker's answer, an array when the request asked for a loop, +// else undefined), sample k is exported over loops[k], and a job whose answer has no usable window for a sample fails. async function exportListed(listed, artifactDir, clipName, timeoutMs, loops = undefined) { const bvhs = []; for (const file of Array.isArray(listed) ? listed : []) { @@ -659,9 +659,13 @@ async function exportListed(listed, artifactDir, clipName, timeoutMs, loops = un bvhs.push(path.resolve(artifactDir, file)); } if (bvhs.length === 0) return { clips: [], error: "fbx-export-failed (no bvh)" }; - bvhs.sort(); + // A sample's own index is in its file name (motion_NN.bvh, more digits past 99), so the order and the window come + // from that number and never from sorting the names as text. + const sampleOf = (bvh) => Number(/_(\d+)\.bvh$/.exec(bvh)?.[1] ?? 0); + bvhs.sort((a, b) => sampleOf(a) - sampleOf(b) || (a < b ? -1 : a > b ? 1 : 0)); const clips = []; - for (const [index, bvh] of bvhs.entries()) { + for (const bvh of bvhs) { + const index = sampleOf(bvh); const name = bvhs.length === 1 ? clipName : `${clipName}_${String(index).padStart(2, "0")}`; const window = loops === undefined ? null : loops[index]; if (window !== null && !(Number.isInteger(window?.first) && Number.isInteger(window?.last) && window.first >= 0 && window.last > window.first)) {