claude-authored-v3-pipeline.integration.test.mjs 6.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. import assert from "node:assert/strict";
  2. import test from "node:test";
  3. import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
  4. import { join } from "node:path";
  5. import sharp from "sharp";
  6. import { createRun } from "../../core/run-paths.mjs";
  7. import { contractSha256 } from "./reference-first-v3-contracts.mjs";
  8. import { boardSpec, setPlan } from "./reference-first-v3-fixtures.mjs";
  9. import { generateClaudeAuthoredReferences, generateClaudeAuthoredSet, startClaudeAuthoredV3 } from "./image-set-runner.mjs";
  10. const visualProduct = { category: "鞋", form: "自然直立", colors: ["黑色"], material_and_light_cues: ["哑光鞋面"], identity_cues: ["白色鞋底", "黑色鞋带"], natural_support_and_use: ["地面穿着"] };
  11. const png = () => sharp({ create: { width: 320, height: 180, channels: 3, background: "#ddd" } }).png().toBuffer();
  12. test("V3 stages keep Qwen descriptive, Claude prompts dynamic, and set references limited to 2 generated assets", async () => {
  13. const paths = await createRun();
  14. try {
  15. const original = join(paths.attachments_directory, "original.png"); await writeFile(original, await png());
  16. const originalObservation = { schema_version: "1.0", purpose: "claude_visual_input_not_creative_plan", run_id: paths.run_id, input_asset_sha256: "originalhash", product: visualProduct };
  17. const start = await startClaudeAuthoredV3({ paths, originalReference: original, productionBrief: { instruction: "淘宝七图", platform: "taobao", aspectRatio: "1:1" }, observeOriginal: async ({ outputFile }) => { await writeFile(outputFile, JSON.stringify(originalObservation)); return { observation: originalObservation, model_calls: 1 }; } });
  18. assert.equal(start.status, "ready_for_reference_authoring"); assert.equal(start.cost.confirmed_image_cost_cny, 0);
  19. const spec = boardSpec(paths.run_id, start.original_visual_observation_sha256);
  20. const dynamicPrompt = `生成一张16:9横版当前鞋类商品设定图。${"根据当前黑色鞋面、白色鞋底、地面自然支撑关系设计主视角、多角度、材质细节、穿着和玄关收纳场景;保持真实商业摄影与无新增文字。".repeat(12)}`;
  21. const authoring = { schema_version: "1.0", authored_by: "claude", run_id: paths.run_id, source_observation_sha256: start.original_visual_observation_sha256, setting_board_spec: spec, setting_board_visual_prompt: dynamicPrompt };
  22. const refExecute = async (_script, args) => {
  23. const files = [join(paths.reference_assets_directory, "three.png"), join(paths.reference_assets_directory, "board.png")];
  24. await Promise.all(files.map(async (file) => writeFile(file, await png())));
  25. const summary = { status: "complete", local_images: files, remote_images: ["https://img.test/three.png", "https://img.test/board.png"], cost_range_cny: { min: 0.44, max: 0.44 } };
  26. const output = args[args.indexOf("--output") + 1]; await writeFile(output, JSON.stringify({ status: "complete", results: files.map((file, index) => ({ status: "complete", local_images: [file], images: [summary.remote_images[index]], cost_cny: 0.22 })) })); return JSON.stringify(summary);
  27. };
  28. const referenceObservation = { schema_version: "1.0", purpose: "claude_reference_visual_input_not_creative_plan", run_id: paths.run_id, input_assets_sha256: ["a", "b"], assets: [{ asset_role: "three_view", product: visualProduct }, { asset_role: "setting_board_visual", product: visualProduct }] };
  29. const refs = await generateClaudeAuthoredReferences({ paths, referenceAuthoring: authoring, executeNode: refExecute, observeReferences: async ({ outputFile }) => { await writeFile(outputFile, JSON.stringify(referenceObservation)); return { observation: referenceObservation, model_calls: 1 }; } });
  30. assert.equal(refs.status, "ready_for_set_authoring");
  31. const refPlan = JSON.parse(await readFile(paths.reference_asset_plan, "utf8")); assert.equal(refPlan.images[1].generation_prompt, dynamicPrompt); assert.equal(refPlan.reference_authoring.authored_by, "claude");
  32. const plan = setPlan(paths.run_id, refs.reference_visual_observation_sha256);
  33. const setExecute = async (_script, args) => {
  34. const files = await Promise.all(Array.from({ length: 7 }, async (_, index) => { const file = join(paths.images_directory, `${index + 1}.png`); await writeFile(file, await png()); return file; }));
  35. const summary = { status: "complete", local_images: files, remote_images: files.map((_, index) => `https://img.test/${index + 1}.png`), cost_range_cny: { min: 1.54, max: 1.54 } };
  36. const output = args[args.indexOf("--output") + 1]; await writeFile(output, JSON.stringify(summary)); return JSON.stringify(summary);
  37. };
  38. const layoutPages = Array.from({ length: 7 }, (_, index) => ({ slot_id: String(index + 1).padStart(2, "0"), product_region: { status: "unverifiable", boxes: [] }, recommended_text_safe_regions: { status: "unverifiable", boxes: [] } }));
  39. const designedFiles = Array.from({ length: 7 }, (_, index) => ({ file: join(paths.run_directory, "designed", `${index + 1}.png`) }));
  40. const completed = await generateClaudeAuthoredSet({ paths, setPlan: plan, executeNode: setExecute, detectLayout: async () => ({ regions: { pages: layoutPages }, model_calls: 1 }), postprocessDesign: async () => { await mkdir(join(paths.run_directory, "designed"), { recursive: true }); await Promise.all(designedFiles.map(async (item) => writeFile(item.file, await png()))); return { files: designedFiles, manifest_file: join(paths.run_directory, "designed", "manifest.json") }; } });
  41. assert.equal(completed.status, "complete"); assert.equal(completed.original_reference_used_for_set, false);
  42. const promptPlan = JSON.parse(await readFile(paths.prompt_plan, "utf8")); assert.ok(promptPlan.images.every((image) => image.reference_images.length === 2)); assert.ok(promptPlan.images.every((image) => !image.reference_images.includes(original)));
  43. assert.equal(contractSha256(referenceObservation), refs.reference_visual_observation_sha256);
  44. } finally { await rm(paths.run_directory, { recursive: true, force: true }); }
  45. });