CODE · 5.3 KB
scripts/generate-layered-real-e2e.mjs
Workspace snapshot · 08/31 23:51
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { buildVisualPlan } from "../src/visual-strategy.js";
import { buildExportBundle } from "../src/export-bundle.js";
const engine = "https://ai-banner.agent-venture-studio.com/api/generate-banners";
const output = new URL("../output/real-e2e/", import.meta.url);
const stateUrl = new URL("layered-generation-state.json", output);
const layerDirectory = new URL("layers/", output);
const design = JSON.parse(await readFile(new URL("design.json", output), "utf8"));
const plan = buildVisualPlan({ project: design.project, sections: design.sections, direction: design.tone, purpose: "implementation" });
let state;
try {
state = JSON.parse(await readFile(stateUrl, "utf8"));
} catch {
state = { createdAt: new Date().toISOString(), jobs: [] };
}
if (process.env.SEED_JOB_ID && !state.jobs.some((item) => item.jobId === process.env.SEED_JOB_ID)) {
const visual = plan[0];
state.jobs.push({ jobId: process.env.SEED_JOB_ID, sectionId: visual.sectionId, visualId: visual.visualId, layer: visual.layer, status: "QUEUED" });
}
const persist = () => writeFile(stateUrl, JSON.stringify(state, null, 2), "utf8");
await persist();
if (!process.argv.includes("--poll-only")) for (const [index, visual] of plan.entries()) {
if (state.jobs.some((item) => item.visualId === visual.visualId)) continue;
const { sectionId, visualId, layer, ...payload } = visual;
const response = await fetch(engine, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
...payload,
copy: layer === "content" && !payload.copy?.cta ? { ...(payload.copy || {}), cta: "" } : payload.copy,
textless: layer === "background",
colorScheme: "カラフル",
useCase: layer === "background" ? "Webサイト セクション背景" : (visual.type === "hero" ? "Webサイト メインビジュアル" : "Webサイト セクション画像"),
useCaseId: visual.type === "hero" ? "web-hero" : "web-banner",
directions: [visual.direction],
}),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data?.jobs?.[0]?.jobId) {
state.queueError = { visualId, status: response.status, error: data?.error || "QUEUE_FAILED" };
await persist();
if (response.status === 502) {
console.warn(`deferred ${visualId} HTTP_502`);
continue;
}
throw new Error(`${visualId}: ${data?.error || `HTTP_${response.status}`}`);
}
state.jobs.push({ jobId: data.jobs[0].jobId, sectionId, visualId, layer, textIntegrated: visual.textIntegrated, status: data.jobs[0].status || "QUEUED" });
await persist();
console.log(`queued ${state.jobs.length}/${plan.length} ${visualId}`);
}
if (process.argv.includes("--queue-only")) process.exit(0);
await mkdir(layerDirectory, { recursive: true });
for (let attempt = 0; attempt < 720; attempt += 1) {
const pending = state.jobs.filter((item) => !["COMPLETED", "FAILED"].includes(item.status));
for (let index = 0; index < pending.length; index += 3) {
const batch = pending.slice(index, index + 3);
const response = await fetch(`${engine}?jobIds=${encodeURIComponent(batch.map((item) => item.jobId).join(","))}`);
const data = await response.json().catch(() => null);
if (!response.ok) throw new Error(data?.error || `POLL_${response.status}`);
for (const job of data?.jobs || []) {
const record = state.jobs.find((item) => item.jobId === (job.id || job.jobId));
if (!record) continue;
record.status = job.status;
record.error = job.error_summary || job.errorSummary || null;
record.signedUrl = job.output?.artifacts?.[0]?.signedUrl || job.artifact?.signedUrl || job.output?.artifact?.signedUrl || record.signedUrl;
}
}
state.updatedAt = new Date().toISOString();
await persist();
const completed = state.jobs.filter((item) => item.status === "COMPLETED").length;
const failed = state.jobs.filter((item) => item.status === "FAILED");
console.log(`progress ${completed}/${state.jobs.length}${failed.length ? ` failed=${failed.length}` : ""}`);
if (failed.length) throw new Error(failed.map((item) => `${item.visualId}:${item.error || "FAILED"}`).join("\n"));
if (completed === state.jobs.length) break;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
if (!state.jobs.every((item) => item.status === "COMPLETED" && item.signedUrl)) throw new Error("LAYERED_IMAGE_TIMEOUT");
const records = [];
for (const item of state.jobs) {
const response = await fetch(item.signedUrl);
if (!response.ok) throw new Error(`DOWNLOAD_${item.visualId}`);
const contentType = response.headers.get("content-type") || "image/png";
const extension = contentType.includes("webp") ? "webp" : contentType.includes("jpeg") ? "jpg" : "png";
const filename = `${item.sectionId}-${item.layer}.${extension}`;
await writeFile(new URL(filename, layerDirectory), Buffer.from(await response.arrayBuffer()));
records.push({ ...item, src: item.signedUrl });
}
const bundle = await buildExportBundle({
project: design.project,
sections: design.sections,
tone: design.tone,
images: records,
publicUrl: design.project?.answers?.canonicalUrl || "",
});
await writeFile(new URL("landing-page-layered-package.zip", output), Buffer.from(await bundle.arrayBuffer()));
console.log(`complete ${records.length} layered images`);