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
2 changes: 1 addition & 1 deletion api/community.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default async function handler(req, res) {
if (op === "ref-done" || op === "compared") {
if (!bearer(req, process.env.COMMUNITY_RENDER_SECRET)) 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 })
? 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 });
return json(res, r.ok ? 200 : 400, r);
}
Expand Down
15 changes: 11 additions & 4 deletions src/community.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export async function readSource(src, fetcher = fetch) {
} catch { /* page layout changed: Claude reads the page itself */ }
}
if (out.title) out.title = text(out.title);
if (!out.video) out.error = `The page gave no video (${html.length} bytes${/cf-chl|challenge-platform|Just a moment/.test(html) ? ", a bot check" : ""}).`;
return out;
}

Expand All @@ -138,10 +139,11 @@ async function reference(src, video, { now, dispatch, wait }) {
let meta = await getJson(refPath(src.id));
const stuck = m => m && m.status !== "ready" && now - Date.parse(m.requested) > RETRY_MS;
if (!meta || stuck(meta)) {
if (!video || !SOURCES[src.host].video.test(video)) return { status: "failed", reason: "The video of this page could not be found." };
// a page that does not show its video to this server (a bot check, say) is read by the job itself
if (video && !SOURCES[src.host].video.test(video)) video = null;
meta = { status: "preparing", video, requested: iso(now) };
await putJson(refPath(src.id), meta);
if (!(await dispatch("reference", src.id, { video }).catch(() => false))) {
if (!(await dispatch("reference", src.id, video ? { video } : { page: src.url }).catch(() => false))) {
meta = { ...meta, status: "failed", reason: "The video could not be sent to be read right now; try again later." };
await putJson(refPath(src.id), meta);
}
Expand All @@ -157,7 +159,7 @@ export async function getRecreationBrief(args, { fetcher = fetch, dispatch = dis
if (holds(existing, now)) return { ok: true, alreadyExists: true, ...publicRecord(existing), message: "This video is already a Community template (or one is being made from it). Do not recreate it: give the user this status." };
const page = await readSource(src, fetcher);
const ref = await reference(src, page.video, { now, dispatch, wait });
if (ref.status === "failed") return { ok: false, template: src.id, error: `${ref.reason || "The video could not be read."} A template is only made when it can be compared with its original video, so this one cannot be recreated now.` };
if (ref.status === "failed") return { ok: false, template: src.id, error: `${ref.reason || "The video could not be read."}${page.error ? ` (${page.error})` : ""} A template is only made when it can be compared with its original video, so this one cannot be recreated now.` };
const out = {
ok: true, alreadyExists: false, template: src.id,
source: { site: src.site, page: src.page, id: src.slug, url: src.url, title: page.title, author: page.author, authorUrl: page.authorUrl, video: page.video },
Expand Down Expand Up @@ -346,9 +348,14 @@ export async function putRef(id, file, bytes) {
return { ok: true };
}
/** Reference job finished: the video's size, duration and the times of each sheet's frames. */
export async function finishRef(id, { ok, reason, duration, width, height, sheets }) {
export async function finishRef(id, { ok, reason, duration, width, height, sheets, video }) {
const m = await getJson(refPath(id));
if (m?.status !== "preparing") return { ok: false, error: "No reference is being prepared." };
if (ok && !m.video) {
const host = Object.values(SOURCES).find(s => s.video.test(String(video)));
if (!host) return { ok: false, error: "video must be the source page's video." };
m.video = video;
}
if (ok && !(num(duration, 0.5, 600) && num(width, 16, 8192) && num(height, 16, 8192) && sheets && times(sheets.general, duration) && (sheets.inicio == null || times(sheets.inicio, duration)))) return { ok: false, error: "duration, width, height and sheets.general are required." };
const next = ok ? { ...m, status: "ready", duration, width, height, sheets: { general: sheets.general, ...(sheets.inicio ? { inicio: sheets.inicio } : {}) }, reason: null }
: { ...m, status: "failed", reason: String(reason || "The video could not be read.").slice(0, 300) };
Expand Down
22 changes: 18 additions & 4 deletions tests/community.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,25 @@ test("comparisons are limited and an abandoned proposal frees its video", async
const other = await proposeTemplate({ url, compositions: compositions(), card }, { caller: "3.3.3.3", ...opts, now: later });
assert.equal(other.accepted, true); assert.notEqual(other.code, r.code);
});
test("a page without a readable video is not recreated", async () => {
test("a page that hides its video from the server is read by the reference job", async () => {
setStore(mem()); jobs.length = 0;
const noVideo = async () => ({ ok: true, text: async () => "<html>Just a moment...</html>" });
const url = "https://prompt-motion.com/parkerrex-1a54fe", id = "c-pm-parkerrex-1a54fe";
const b = await getRecreationBrief({ url }, { ...opts, fetcher: noVideo });
assert.equal(b.reference.status, "preparing");
assert.deepEqual(jobs.at(-1), { kind: "reference", id, page: url });
assert.equal((await finishRef(id, { ok: true, duration: 20, width: 1920, height: 1080, sheets: { general: [1] }, video: "https://evil.com/x.mp4" })).ok, false);
assert.equal((await finishRef(id, { ok: true, duration: 20, width: 1920, height: 1080, sheets: { general: [1] }, video: "https://media.prompt-motion.com/x/v.mp4" })).ok, true);
await proposeTemplate({ url, compositions: compositions(), card }, { caller: "4.4.4.4", ...opts, fetcher: noVideo });
assert.equal(jobs.at(-1).video, "https://media.prompt-motion.com/x/v.mp4");
});
test("a job that cannot read the video fails the brief", async () => {
setStore(mem());
const noVideo = async () => ({ ok: true, text: async () => PM.replace(/"contentUrl":"[^"]+",/, "") });
const b = await getRecreationBrief({ url: "https://prompt-motion.com/parkerrex-1a54fe" }, { ...opts, fetcher: noVideo });
assert.equal(b.ok, false); assert.match(b.error, /compared/);
const id = "c-pm-parkerrex-1a54fe";
await getRecreationBrief({ url: "https://prompt-motion.com/parkerrex-1a54fe" }, opts);
await finishRef(id, { ok: false, reason: "el video respondió 403" });
const b = await getRecreationBrief({ url: "https://prompt-motion.com/parkerrex-1a54fe" }, opts);
assert.equal(b.ok, false); assert.match(b.error, /403.*compared/);
});
test("daily limit per caller", async () => {
setStore(mem());
Expand Down
Loading