Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/community.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function body(req) {
* POST /api/community?op=ref&id=&file= reference job uploads one frame sheet (the render workflow: GitHub OIDC)
* POST /api/community?op=ref-done {id, ok, reason, duration, width, height, sheets} (the render workflow: GitHub OIDC)
* POST /api/community?op=compare&id=&file= compare job uploads sheet.jpg or comparacion.mp4 (the render workflow: GitHub OIDC)
* POST /api/community?op=compared {id, round, ok, reason, times} (the render workflow: GitHub OIDC)
* POST /api/community?op=compared {id, round, ok, reason, times, metrics} (the render workflow: GitHub OIDC)
* POST /api/community?op=media&id=&file= render job uploads one preview file (the render workflow: GitHub OIDC)
* POST /api/community?op=finish {id, ok, reason, commit} render or publishing job is done (the render workflow: GitHub OIDC)
* POST /api/community?op=retire {id, reason} a moderator takes a Community one out of the gallery (a moderator code)
Expand Down Expand Up @@ -108,7 +108,7 @@ export default async function handler(req, res) {
if (!(await renderJob(req))) return json(res, 401, { error: "Unauthorized" });
const r = op === "ref-done"
? await finishRef(String(data.id), { ok: data.ok === true, reason: data.reason, duration: data.duration, width: data.width, height: data.height, sheets: data.sheets, video: data.video })
: await finishCompare(String(data.id), { round: data.round, ok: data.ok === true, reason: data.reason, times: data.times });
: await finishCompare(String(data.id), { round: data.round, ok: data.ok === true, reason: data.reason, times: data.times, metrics: data.metrics });
return json(res, r.ok ? 200 : 400, r);
}
if (op === "retire") {
Expand Down
2 changes: 1 addition & 1 deletion src/brief.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 19 additions & 5 deletions src/community.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ const UPLOAD_MS = 60 * 60e3; // an upload not finished in this time is sta
export const PART_BYTES = 3_000_000; // the original is sent in parts that fit a function's request body
export const MAX_PARTS = 20;
export const REF_FILES = ["general.jpg", "inicio.jpg"];
export const COMPARE_FILES = ["sheet.jpg", "comparacion.mp4"];
export const COMPARE_FILES = ["sheet.jpg", "diff.jpg", "comparacion.mp4"];
/** Nota mínima (0–100, SSIM + color medidos por el job) para que Claude reciba la lámina; por debajo solo recibe el informe en texto. */
export const PASS_SCORE = Math.max(0, Math.min(100, Number(process.env.TEMPLATE_PASS_SCORE) || 60));
const cleanMetrics = m => {
if (!m || typeof m !== "object" || !num(m.score, 0, 100)) return null;
const pairs = Array.isArray(m.pairs) ? m.pairs.slice(0, 40).filter(p => p && num(p.t, 0, 600) && num(p.score, 0, 100)).map(p => ({ t: p.t, score: Math.round(p.score), ssim: Number(p.ssim) || 0, color: Number(p.color) || 0, pixels: Number(p.pixels) || 0 })) : [];
return { score: Math.round(m.score), report: String(m.report || "").slice(0, 900), pairs };
};
const MAX_DURATION = 45;
const MAX_COMPOSITION_BYTES = 1.2 * 1024 * 1024;
const RATIOS = ["1-1", "3-4", "4-3", "9-16", "16-9"];
Expand Down Expand Up @@ -378,9 +385,16 @@ export async function getProposalStatus(args, { wait = WAIT_MS } = {}) {
const left = Math.max(0, Math.min(MAX_ROUNDS - r.compare.done, MAX_SUBMISSIONS - r.compare.round));
const compare = { round: r.compare.round, ok: r.compare.ok, reason: r.compare.reason, variant: r.variant, times: r.compare.times, revisionsLeft: left };
if (!r.compare.ok) return { ...out, compare, message: `The comparison failed: ${r.compare.reason} Fix it and send the three compositions again with propose_template and this code.` };
const mt = r.compare.metrics;
if (mt) compare.score = mt.score;
// filtro sin LLM: por debajo de la nota mínima no hace falta mirar la lámina, el informe dice qué corregir
if (mt && mt.score < PASS_SCORE && left > 0) return {
...out, compare: { ...compare, passScore: PASS_SCORE, report: mt.report },
message: `Measured without images: score ${mt.score}/100, below ${PASS_SCORE}. ${mt.report} Fix those moments (layout, sizes, positions, colors, timing) in all three compositions and call propose_template again with this code (${left} left). The sheet is only shown once the score reaches ${PASS_SCORE}.`
};
const sheet = await jpg(`community/compare/${r.id}/sheet.jpg`);
return {
...out, compare: { ...compare, how: "In the attached sheet each pair is one moment: left the original, right your recreation rendered by the Studio engine (times given as [original, recreation] in seconds; the recreation's time is scaled to its duration)." },
...out, compare: { ...compare, ...(mt ? { passScore: PASS_SCORE, report: mt.report } : {}), how: "In the attached sheet each pair is one moment: left the original, right your recreation rendered by the Studio engine (times given as [original, recreation] in seconds; the recreation's time is scaled to its duration)." },
message: `Compare every pair: layout, sizes, positions, timing, colors, typefaces and texts. If something differs, fix it in all three compositions and call propose_template again with this code (${left} left). When it truly looks like the original, call publish_template with this code and notes on what is consciously different and why.`,
_images: sheet ? [sheet] : []
};
Expand Down Expand Up @@ -501,14 +515,14 @@ export async function putCompare(id, file, bytes) {
return { ok: true };
}
/** Compare job finished: Claude reviews the result. A job of an older round is ignored. */
export async function finishCompare(id, { round, ok, reason, times: pairs }, now = Date.now()) {
export async function finishCompare(id, { round, ok, reason, times: pairs, metrics }, now = Date.now()) {
const r = await getJson(`community/records/${id}.json`);
if (r?.status !== "comparing") return { ok: false, error: "Not being compared." };
if (round !== r.compare.round) return { ok: false, error: "That round was replaced by a newer one." };
const valid = Array.isArray(pairs) && pairs.length <= 40 && pairs.every(p => Array.isArray(p) && p.length === 2 && p.every(t => num(t, 0, 600)));
if (ok && !valid) return { ok: false, error: "times must be the [original, recreation] pairs of the sheet." };
r.status = "review"; r.updated = iso(now);
r.compare = ok ? { ...r.compare, ok: true, reason: null, times: pairs, done: r.compare.done + 1 } : { ...r.compare, ok: false, reason: String(reason || "The recreation could not be rendered.").slice(0, 300), times: null };
r.compare = ok ? { ...r.compare, ok: true, reason: null, times: pairs, metrics: cleanMetrics(metrics), done: r.compare.done + 1 } : { ...r.compare, ok: false, reason: String(reason || "The recreation could not be rendered.").slice(0, 300), times: null };
await putJson(`community/records/${id}.json`, r);
return { ok: true, status: r.status };
}
Expand Down Expand Up @@ -552,7 +566,7 @@ export async function finishRender(id, { ok, reason, commit, pr }, now = Date.no
export async function readRecord(id) {
if (!/^c-[a-z0-9-]{3,62}$/.test(id)) return null;
const r = await getJson(`community/records/${id}.json`);
return r && r.official ? { id: r.id, card: r.card, source: r.source, notes: r.notes ?? "", official: r.official } : null;
return r && r.official ? { id: r.id, card: r.card, source: r.source, notes: r.notes ?? "", official: r.official, score: r.compare?.metrics?.score ?? null, report: r.compare?.metrics?.report ?? null } : null;
}

/** Owner: takes a template out of the gallery (its record stays, so the same video is not proposed again). */
Expand Down
18 changes: 17 additions & 1 deletion tests/community.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { parseSource, readSource, getRecreationBrief, proposeTemplate, getProposalStatus, publishTemplate, finishRender, finishRef, finishCompare, putRef, putCompare, putMedia, putUpload, finishUpload, readUpload, retire, readIndex, readTemplate, readRecord, setStore, moderators, moderatorOf, slugOf, MAX_PER_DAY, MAX_ROUNDS } from "../src/community.js";
import { parseSource, readSource, getRecreationBrief, proposeTemplate, getProposalStatus, publishTemplate, finishRender, finishRef, finishCompare, putRef, putCompare, putMedia, putUpload, finishUpload, readUpload, retire, readIndex, readTemplate, readRecord, setStore, moderators, moderatorOf, slugOf, MAX_PER_DAY, MAX_ROUNDS, PASS_SCORE } from "../src/community.js";
import { getTemplate } from "../src/templates.js";
import handler from "../api/community.js";

Expand Down Expand Up @@ -200,6 +200,22 @@ test("a job that cannot read the video fails the brief (sources without an uploa
const b = await getRecreationBrief({ ...A, url }, o);
assert.equal(b.ok, false); assert.match(b.error, /403.*compared/);
});
test("a low measured score answers in text, without the sheet", async () => {
setStore(mem()); jobs.length = 0;
const url = "https://prompt-motion.com/parkerrex-1a54fe", id = "c-pm-parkerrex-1a54fe";
await brief({ ...A, url }, opts); await referenceReady(id);
const r = await proposeTemplate({ ...A, url, compositions: compositions(), card }, opts);
await putCompare(id, "sheet.jpg", JPG);
const low = { score: PASS_SCORE - 20, report: "Nota baja. 2.50 s: composición distinta", pairs: [{ t: 2.5, tr: 2.5, score: 30, ssim: 0.2, color: 0.7 }] };
assert.equal((await finishCompare(id, { round: 1, ok: true, times: [[2.5, 2.5]], metrics: low })).ok, true);
const st = await getProposalStatus({ code: r.code }, { wait: 0 });
assert.equal(st.compare.score, PASS_SCORE - 20); assert.equal(st._images, undefined); assert.match(st.message, /2\.50 s: composición distinta/);
assert.equal((await proposeTemplate({ ...A, url, code: r.code, compositions: compositions(), card }, opts)).accepted, true);
const high = { ...low, score: PASS_SCORE + 10 };
await finishCompare(id, { round: 2, ok: true, times: [[2.5, 2.5]], metrics: high });
const ok = await getProposalStatus({ code: r.code }, { wait: 0 });
assert.equal(ok.compare.score, PASS_SCORE + 10); assert.equal(ok._images.length, 1);
});
test("daily limit per caller", async () => {
setStore(mem());
const ids = Array.from({ length: MAX_PER_DAY + 1 }, (_, i) => `https://prompt-motion.com/autor-${i}abc`);
Expand Down
Loading