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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-25 18:32:26 +01:00
commit d35b9e2c31
2 changed files with 51 additions and 4 deletions

View file

@ -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 {

View file

@ -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
// <clipName>_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.
// <clipName>_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)) {