← GPT-AI-LP
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`);