| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- import { readFile } from "node:fs/promises";
- const filename = process.argv[2];
- if (!filename) throw new Error("Usage: node validate-plan.mjs <prompt-plan.json>");
- const plan = JSON.parse(await readFile(filename, "utf8"));
- const errors = [];
- const isText = (value) => typeof value === "string" && value.trim().length > 0;
- const asArray = (value) => Array.isArray(value) ? value : [];
- const countValues = (values) => new Map(values.map((value) => [
- value,
- values.filter((item) => item === value).length,
- ]));
- if (plan.schema_version !== "0.6") errors.push("schema_version must be 0.6");
- if (!isText(plan.run_id)) errors.push("run_id is required");
- if (!new Set(["ready", "needs_input"]).has(plan.status)) errors.push("status must be ready or needs_input");
- if (plan.user_output_mode !== "images_only") errors.push("user_output_mode must be images_only");
- if (!isText(plan.request?.instruction)) errors.push("request.instruction is required");
- if (!Array.isArray(plan.request?.reference_images)) errors.push("request.reference_images must be an array");
- if (!isText(plan.context?.platform)) errors.push("context.platform is required");
- if (!new Set(["available", "unavailable"]).has(plan.context?.voc?.status)) errors.push("context.voc.status is invalid");
- if (!Array.isArray(plan.context?.voc?.findings)) errors.push("context.voc.findings must be an array");
- const understanding = plan.product_understanding ?? {};
- if (!isText(understanding.category)) errors.push("product_understanding.category is required");
- if (!isText(understanding.product_name?.value)) errors.push("product_understanding.product_name.value is required");
- if (!isText(understanding.product_name?.basis)) errors.push("product_understanding.product_name.basis is required");
- if (!new Set(["high", "medium", "low", "unknown"]).has(understanding.product_name?.confidence)) {
- errors.push("product_understanding.product_name.confidence is invalid");
- }
- for (const key of [
- "visible_facts",
- "verified_visible_facts",
- "inferred_attributes",
- "unknown_features",
- "provided_facts",
- "candidate_selling_points",
- "creative_positioning",
- "suggested_audiences",
- "suggested_scenes",
- "unsupported_claims",
- "unknowns",
- ]) {
- if (!Array.isArray(understanding[key])) errors.push(`product_understanding.${key} must be an array`);
- }
- if (!understanding.visible_specs || typeof understanding.visible_specs !== "object" || Array.isArray(understanding.visible_specs)) {
- errors.push("product_understanding.visible_specs must be an object");
- }
- const identityManifest = understanding.identity_manifest ?? {};
- for (const key of ["must_preserve", "must_not_add", "unknown_do_not_assert"]) {
- if (!Array.isArray(identityManifest[key])) errors.push(`product_understanding.identity_manifest.${key} must be an array`);
- }
- if (asArray(identityManifest.must_preserve).length < 3) {
- errors.push("product_understanding.identity_manifest.must_preserve requires at least three features");
- }
- if (!isText(identityManifest.product_lock)) {
- errors.push("product_understanding.identity_manifest.product_lock is required");
- } else {
- const identityLockLength = [...identityManifest.product_lock].length;
- if (identityLockLength < 20 || identityLockLength > 160) {
- errors.push(`product_understanding.identity_manifest.product_lock must be 20..160 characters (${identityLockLength})`);
- }
- }
- const sellingPoints = asArray(understanding.candidate_selling_points);
- const validSellingPointBasisRefs = new Set(["user_instruction"]);
- asArray(understanding.visible_facts).forEach((_, index) => validSellingPointBasisRefs.add(`visible_fact:${index + 1}`));
- asArray(understanding.provided_facts).forEach((_, index) => validSellingPointBasisRefs.add(`provided_fact:${index + 1}`));
- asArray(plan.context?.voc?.findings).forEach((_, index) => validSellingPointBasisRefs.add(`voc:${index + 1}`));
- if (plan.status === "ready" && sellingPoints.length < 2) {
- errors.push("ready plan requires at least two candidate_selling_points");
- }
- const sellingPointIds = [];
- for (const [index, point] of sellingPoints.entries()) {
- if (!isText(point?.id)) errors.push(`candidate_selling_points[${index}].id is required`);
- if (!isText(point?.value)) errors.push(`candidate_selling_points[${index}].value is required`);
- if (!Array.isArray(point?.basis_refs) || point.basis_refs.length === 0) {
- errors.push(`candidate_selling_points[${index}].basis_refs must be non-empty`);
- } else {
- for (const reference of point.basis_refs) {
- if (!validSellingPointBasisRefs.has(reference)) {
- errors.push(`candidate_selling_points[${index}].basis_refs contains unknown reference: ${reference}`);
- }
- }
- }
- if (!new Set(["high", "medium", "low", "unknown"]).has(point?.confidence)) {
- errors.push(`candidate_selling_points[${index}].confidence is invalid`);
- }
- sellingPointIds.push(point?.id);
- }
- if (new Set(sellingPointIds).size !== sellingPointIds.length) {
- errors.push("candidate_selling_points ids must be unique");
- }
- for (const key of ["product_invariants", "visual_system", "prohibited_changes"]) {
- if (!Array.isArray(plan.global_consistency_lock?.[key])) errors.push(`global_consistency_lock.${key} must be an array`);
- }
- const requiredAllocation = { hero: 1, scene: 2, selling_point: 2, decision_support: 2 };
- for (const [key, value] of Object.entries(requiredAllocation)) {
- if (plan.set_blueprint?.business_allocation?.[key] !== value) {
- errors.push(`set_blueprint.business_allocation.${key} must be ${value}`);
- }
- }
- if (!isText(plan.set_blueprint?.visual_thesis)) errors.push("set_blueprint.visual_thesis is required");
- for (const key of ["narrative_arc", "shared_visual_dna", "intentional_variations"]) {
- if (!Array.isArray(plan.set_blueprint?.[key]) || plan.set_blueprint[key].length === 0) {
- errors.push(`set_blueprint.${key} must be a non-empty array`);
- }
- }
- if (!Array.isArray(plan.images)) errors.push("images must be an array");
- if (plan.status === "ready" && plan.images?.length !== 7) errors.push("ready plan must contain exactly seven images");
- const businessCategories = new Set(Object.keys(requiredAllocation));
- const renderFamiliesByCategory = {
- hero: new Set(["isolated_product"]),
- scene: new Set(["environmental_scene", "human_interaction", "usage_demonstration"]),
- selling_point: new Set(["material_demonstration", "feature_demonstration", "annotated_detail"]),
- decision_support: new Set([
- "multi_angle",
- "usage_sequence",
- "package_contents",
- "scale_context",
- "comparison_sequence",
- "structured_information",
- ]),
- };
- const sceneComplexities = new Set(["simple", "medium", "complex"]);
- const moduleNames = [
- "product_lock",
- "image_job",
- "scene",
- "composition",
- "lighting_material",
- "camera_style",
- "text_layout",
- "negative_constraints",
- ];
- const complexityRules = {
- simple: { promptMin: 100, sceneMin: 12 },
- medium: { promptMin: 220, sceneMin: 60 },
- complex: { promptMin: 360, sceneMin: 160 },
- };
- const highRiskClaimTerms = [
- "防水", "抗菌", "临床", "认证", "销量第一", "最畅销", "耐磨", "耐用", "续航",
- "兼容", "承重", "保质期", "无毒", "不刺激", "留香", "香调", "成分", "浓度", "功效",
- ];
- const evidenceText = JSON.stringify([
- plan.request?.instruction,
- ...asArray(understanding.provided_facts),
- ...sellingPoints.map((item) => item.value),
- understanding.visible_specs ?? {},
- ...asArray(plan.context?.voc?.findings),
- ]);
- const knownEvidenceRefs = new Set(["user_instruction"]);
- asArray(understanding.visible_facts).forEach((_, index) => knownEvidenceRefs.add(`visible_fact:${index + 1}`));
- asArray(understanding.provided_facts).forEach((_, index) => knownEvidenceRefs.add(`provided_fact:${index + 1}`));
- asArray(understanding.creative_positioning).forEach((_, index) => knownEvidenceRefs.add(`creative_positioning:${index + 1}`));
- asArray(understanding.suggested_audiences).forEach((_, index) => knownEvidenceRefs.add(`suggested_audience:${index + 1}`));
- asArray(understanding.suggested_scenes).forEach((_, index) => knownEvidenceRefs.add(`suggested_scene:${index + 1}`));
- asArray(plan.context?.voc?.findings).forEach((_, index) => knownEvidenceRefs.add(`voc:${index + 1}`));
- Object.keys(understanding.visible_specs ?? {}).forEach((key) => knownEvidenceRefs.add(`visible_spec:${key}`));
- sellingPointIds.filter(isText).forEach((id) => knownEvidenceRefs.add(id));
- const ids = [];
- const decisions = [];
- const shotSpecs = [];
- const setDesigns = [];
- const creativeBodies = [];
- const categoryValues = [];
- const sellingImages = [];
- const categoryRenderFamilies = new Map();
- const staticProductFamilies = new Set(["isolated_product", "material_demonstration", "annotated_detail"]);
- let staticProductCount = 0;
- const requiredShotText = [
- "narrative_job",
- "composition_family",
- "shot_scale",
- "camera_angle",
- "product_placement",
- "depth_structure",
- "background_family",
- "lighting_family",
- "interaction_mode",
- "information_density",
- ];
- const cameraAxes = new Set(["front", "three_quarter_left", "three_quarter_right", "side", "top", "low", "macro"]);
- const framingModes = new Set(["full_product", "product_in_context", "detail", "multi_panel", "human_action"]);
- const productPoses = new Set(["upright", "laid_flat", "held", "in_use", "multi_view", "detail_crop", "arranged_sequence"]);
- for (const [index, image] of asArray(plan.images).entries()) {
- ids.push(image.id);
- decisions.push(image.decision_subject);
- categoryValues.push(image.business_category);
- if (!isText(image.id)) errors.push(`images[${index}].id is required`);
- if (!isText(image.role)) errors.push(`images[${index}].role is required`);
- if (!businessCategories.has(image.business_category)) {
- errors.push(`images[${index}].business_category is invalid`);
- }
- if (!isText(image.decision_subject)) errors.push(`images[${index}].decision_subject is required`);
- if (!Array.isArray(image.evidence_refs) || image.evidence_refs.length === 0) {
- errors.push(`images[${index}].evidence_refs must be non-empty`);
- } else {
- for (const reference of image.evidence_refs) {
- if (!knownEvidenceRefs.has(reference)) {
- errors.push(`images[${index}].evidence_refs contains unknown reference: ${reference}`);
- }
- }
- }
- if (typeof image.selling_point_id !== "string") errors.push(`images[${index}].selling_point_id must be a string`);
- if (!isText(image.purpose)) errors.push(`images[${index}].purpose is required`);
- if (!Array.isArray(image.voc_refs)) errors.push(`images[${index}].voc_refs must be an array`);
- const setDesign = image.set_design ?? {};
- setDesigns.push(setDesign);
- const allowedRenderFamilies = renderFamiliesByCategory[image.business_category];
- if (!allowedRenderFamilies?.has(setDesign.render_family)) {
- errors.push(`images[${index}].set_design.render_family is invalid for ${image.business_category ?? "unknown"}`);
- }
- if (setDesign.render_family === "structured_information") {
- errors.push(`images[${index}] structured_information is unavailable until a deterministic text compositor is implemented`);
- }
- if (staticProductFamilies.has(setDesign.render_family)) staticProductCount += 1;
- if (businessCategories.has(image.business_category) && isText(setDesign.render_family)) {
- const values = categoryRenderFamilies.get(image.business_category) ?? [];
- values.push(setDesign.render_family);
- categoryRenderFamilies.set(image.business_category, values);
- }
- if (!sceneComplexities.has(setDesign.scene_complexity)) {
- errors.push(`images[${index}].set_design.scene_complexity is invalid`);
- }
- if (!isText(setDesign.unique_visual_hook)) errors.push(`images[${index}].set_design.unique_visual_hook is required`);
- if (!isText(setDesign.contrast_with_previous)) errors.push(`images[${index}].set_design.contrast_with_previous is required`);
- if (image.business_category === "selling_point") {
- sellingImages.push(image);
- if (!sellingPointIds.includes(image.selling_point_id)) {
- errors.push(`images[${index}].selling_point_id must match a candidate_selling_points id`);
- }
- if (!image.evidence_refs?.includes(image.selling_point_id)) {
- errors.push(`images[${index}].evidence_refs must include its selling_point_id`);
- }
- } else if (isText(image.selling_point_id)) {
- errors.push(`images[${index}].selling_point_id is only allowed for selling_point images`);
- }
- for (const key of requiredShotText) {
- if (!isText(image.shot_spec?.[key])) errors.push(`images[${index}].shot_spec.${key} is required`);
- }
- if (!cameraAxes.has(image.shot_spec?.camera_axis)) {
- errors.push(`images[${index}].shot_spec.camera_axis is invalid`);
- }
- if (!framingModes.has(image.shot_spec?.framing_mode)) {
- errors.push(`images[${index}].shot_spec.framing_mode is invalid`);
- }
- if (!productPoses.has(image.shot_spec?.product_pose)) {
- errors.push(`images[${index}].shot_spec.product_pose is invalid`);
- }
- const occupancy = image.shot_spec?.product_occupancy_pct;
- if (!(Number.isFinite(occupancy) && occupancy >= 10 && occupancy <= 95)) {
- errors.push(`images[${index}].shot_spec.product_occupancy_pct must be 10..95`);
- }
- shotSpecs.push(image.shot_spec ?? {});
- const promptModules = image.prompt_modules ?? {};
- for (const moduleName of moduleNames) {
- if (typeof promptModules[moduleName] !== "string") {
- errors.push(`images[${index}].prompt_modules.${moduleName} must be a string`);
- }
- }
- for (const moduleName of ["product_lock", "image_job", "scene", "composition", "lighting_material", "negative_constraints"]) {
- if (!isText(promptModules[moduleName])) errors.push(`images[${index}].prompt_modules.${moduleName} is required`);
- }
- const productLockLength = [...(promptModules.product_lock ?? "")].length;
- if (productLockLength < 20 || productLockLength > 160) {
- errors.push(`images[${index}].prompt_modules.product_lock must be 20..160 characters (${productLockLength})`);
- }
- if (isText(identityManifest.product_lock) && promptModules.product_lock !== identityManifest.product_lock) {
- errors.push(`images[${index}].prompt_modules.product_lock must exactly match product_understanding.identity_manifest.product_lock`);
- }
- const positivePromptText = [
- promptModules.image_job,
- promptModules.scene,
- promptModules.composition,
- promptModules.lighting_material,
- promptModules.camera_style,
- ].filter(Boolean).join(" ");
- for (const unknownFeature of [
- ...asArray(understanding.unknown_features),
- ...asArray(identityManifest.unknown_do_not_assert),
- ].filter(isText)) {
- if (positivePromptText.includes(unknownFeature)) {
- errors.push(`images[${index}] positively asserts unknown product feature: ${unknownFeature}`);
- }
- }
- const absentText = asArray(identityManifest.must_not_add).filter(isText).join(" ").toLowerCase();
- const positiveLower = positivePromptText.toLowerCase();
- const contradictionGroups = [
- { absent: ["纽扣", "button"], positive: ["单排扣", "双排扣", "single-breasted", "double-breasted", "buttoned"] },
- { absent: ["口袋", "pocket"], positive: ["口袋", "pocket"] },
- { absent: ["拉链", "zipper"], positive: ["拉链", "zipper", "zip closure"] },
- { absent: ["腰带", "belt"], positive: ["腰带", "belted", "belt"] },
- ];
- for (const group of contradictionGroups) {
- if (
- group.absent.some((term) => absentText.includes(term))
- && group.positive.some((term) => positiveLower.includes(term))
- ) {
- errors.push(`images[${index}] prompt contradicts identity_manifest.must_not_add`);
- break;
- }
- }
- const usesPanelLayout = /三格|多格|分格|面板|横向序列|纵向序列|separate panels|multi[- ]?panel|\bpanel\b|\bgrid\b/i.test(positivePromptText);
- if (usesPanelLayout && image.shot_spec?.framing_mode !== "multi_panel") {
- errors.push(`images[${index}] uses a panel/grid layout without framing_mode=multi_panel`);
- }
- const complexityRule = complexityRules[setDesign.scene_complexity];
- const sceneLength = [...(promptModules.scene ?? "")].length;
- if (complexityRule && sceneLength < complexityRule.sceneMin) {
- errors.push(`images[${index}].prompt_modules.scene is too short for ${setDesign.scene_complexity} complexity (${sceneLength} < ${complexityRule.sceneMin})`);
- }
- if (setDesign.scene_complexity === "complex") {
- if (!isText(promptModules.camera_style)) {
- errors.push(`images[${index}].prompt_modules.camera_style is required for complex scenes`);
- }
- if (sceneLength <= productLockLength * 1.25) {
- errors.push(`images[${index}] complex scene detail must be longer than the product lock`);
- }
- }
- if (isText(promptModules.text_layout)) {
- errors.push(`images[${index}].prompt_modules.text_layout must be empty until deterministic text compositing is available`);
- }
- if (!isText(image.generation_prompt)) errors.push(`images[${index}].generation_prompt is required`);
- const promptLength = [...(image.generation_prompt ?? "")].length;
- if (complexityRule && promptLength < complexityRule.promptMin) {
- errors.push(`images[${index}].generation_prompt is too short for ${setDesign.scene_complexity} complexity (${promptLength} < ${complexityRule.promptMin})`);
- }
- if (promptLength > 800) errors.push(`images[${index}].generation_prompt exceeds 800 characters (${promptLength})`);
- const normalizedPrompt = (image.generation_prompt ?? "").replace(/\s+/g, "");
- for (const moduleName of moduleNames) {
- const moduleText = (promptModules[moduleName] ?? "").replace(/\s+/g, "");
- if (moduleText && !normalizedPrompt.includes(moduleText)) {
- errors.push(`images[${index}].generation_prompt does not contain prompt_modules.${moduleName}`);
- }
- }
- if (typeof image.negative_prompt !== "string") errors.push(`images[${index}].negative_prompt must be a string`);
- const finalRequestPrompt = image.negative_prompt?.trim()
- ? `${image.generation_prompt}\n禁止:${image.negative_prompt}`
- : image.generation_prompt;
- if ([...(finalRequestPrompt ?? "")].length > 800) {
- errors.push(`images[${index}] final request prompt exceeds 800 characters after negative_prompt is appended`);
- }
- const negativeTokens = (image.negative_prompt ?? "").toLowerCase().split(/[,,、;;\s]+/).filter(Boolean);
- if (negativeTokens.includes("logo") || negativeTokens.includes("文字") || negativeTokens.includes("品牌")) {
- errors.push(`images[${index}].negative_prompt conflicts with required product label; use 额外文字/额外品牌标识 instead`);
- }
- if (!Array.isArray(image.overlay_copy)) {
- errors.push(`images[${index}].overlay_copy must be an array`);
- } else if (image.overlay_copy.length > 0) {
- errors.push(`images[${index}].overlay_copy must stay empty until deterministic text compositing is available`);
- }
- if (!Array.isArray(image.reference_images) || image.reference_images.length === 0) {
- errors.push(`images[${index}].reference_images must contain a product reference`);
- }
- if (image.model_parameters?.adapter !== "jimeng_img_v4") errors.push(`images[${index}].model_parameters.adapter must be jimeng_img_v4`);
- if (!Number.isInteger(image.model_parameters?.width) || !Number.isInteger(image.model_parameters?.height)) {
- errors.push(`images[${index}] requires integer width and height`);
- }
- if (!(image.model_parameters?.scale >= 0 && image.model_parameters.scale <= 1)) {
- errors.push(`images[${index}].model_parameters.scale must be 0..1`);
- }
- if (image.model_parameters?.force_single !== true) errors.push(`images[${index}].model_parameters.force_single must be true`);
- if (!Array.isArray(image.acceptance_checks) || image.acceptance_checks.length === 0) {
- errors.push(`images[${index}].acceptance_checks must be non-empty`);
- }
- creativeBodies.push([
- image.decision_subject,
- setDesign.unique_visual_hook,
- promptModules.image_job,
- promptModules.scene,
- promptModules.composition,
- ].filter(Boolean).join(" ").replace(/\s+/g, "").toLowerCase());
- const claimText = [
- promptModules.image_job,
- promptModules.scene,
- ...asArray(image.overlay_copy),
- ].filter(Boolean).join(" ");
- for (const claim of asArray(understanding.unsupported_claims).filter(isText)) {
- if (claimText.includes(claim)) errors.push(`images[${index}] uses unsupported claim: ${claim}`);
- }
- for (const term of highRiskClaimTerms) {
- if (claimText.includes(term) && !evidenceText.includes(term)) {
- errors.push(`images[${index}] uses unsupported high-risk claim term: ${term}`);
- }
- }
- }
- if (plan.status === "ready") {
- const expected = ["01", "02", "03", "04", "05", "06", "07"];
- if (JSON.stringify(ids) !== JSON.stringify(expected)) errors.push("image IDs must be ordered 01 through 07");
- if (plan.images?.[0]?.business_category !== "hero") errors.push("images[0] must be the hero image");
- if (plan.images?.[0]?.set_design?.render_family !== "isolated_product") {
- errors.push("images[0] must use isolated_product");
- }
- const categoryCounts = countValues(categoryValues);
- for (const [category, required] of Object.entries(requiredAllocation)) {
- if ((categoryCounts.get(category) ?? 0) !== required) {
- errors.push(`business_category ${category} must appear exactly ${required} time(s)`);
- }
- }
- if (new Set(decisions).size !== decisions.length) errors.push("decision_subject must be unique across all seven images");
- if (sellingImages.length === 2 && sellingImages[0].selling_point_id === sellingImages[1].selling_point_id) {
- errors.push("selling_point images must use different selling_point_id values");
- }
- for (const category of ["scene", "selling_point", "decision_support"]) {
- const values = categoryRenderFamilies.get(category) ?? [];
- if (values.length === 2 && new Set(values).size !== 2) {
- errors.push(`${category} images must use different render_family values`);
- }
- }
- if (staticProductCount > 2) {
- errors.push(`static product render families may appear at most twice across the set (${staticProductCount})`);
- }
- const diversityRules = [
- ["narrative_job", 7, 1],
- ["composition_family", 5, 2],
- ["shot_scale", 4, 2],
- ["camera_angle", 4, 3],
- ["background_family", 5, 2],
- ["lighting_family", 4, 2],
- ["interaction_mode", 5, 2],
- ["camera_axis", 4, 2],
- ["framing_mode", 4, 2],
- ];
- for (const [field, minimumUnique, maximumRepeats] of diversityRules) {
- const values = shotSpecs.map((spec) => spec?.[field]).filter(isText);
- const counts = countValues(values);
- if (new Set(values).size < minimumUnique) errors.push(`shot_spec.${field} requires at least ${minimumUnique} distinct values`);
- for (const [value, count] of counts) {
- if (count > maximumRepeats) errors.push(`shot_spec.${field} repeats too often: ${value} (${count})`);
- }
- }
- const framingCounts = countValues(shotSpecs.map((spec) => spec?.framing_mode).filter(isText));
- if ((framingCounts.get("detail") ?? 0) > 1) {
- errors.push("shot_spec.framing_mode=detail may appear at most once across the set");
- }
- if ((framingCounts.get("multi_panel") ?? 0) > 1) {
- errors.push("shot_spec.framing_mode=multi_panel may appear at most once across the set");
- }
- const occupancies = shotSpecs.map((spec) => spec?.product_occupancy_pct).filter(Number.isFinite);
- if (occupancies.length === 7 && Math.max(...occupancies) - Math.min(...occupancies) < 35) {
- errors.push("product occupancy range must span at least 35 percentage points across the set");
- }
- const nonHeroSignatures = shotSpecs.slice(1).map((spec) => [
- spec.camera_axis,
- spec.framing_mode,
- spec.product_pose,
- ].join("|"));
- if (new Set(nonHeroSignatures).size !== nonHeroSignatures.length) {
- errors.push("all non-hero images require distinct camera_axis + framing_mode + product_pose visual signatures");
- }
- for (let left = 1; left < shotSpecs.length; left += 1) {
- for (let right = left + 1; right < shotSpecs.length; right += 1) {
- const a = shotSpecs[left];
- const b = shotSpecs[right];
- if (
- a.camera_axis === b.camera_axis
- && a.framing_mode === b.framing_mode
- && Math.abs(a.product_occupancy_pct - b.product_occupancy_pct) < 15
- ) {
- errors.push(`images ${ids[left]} and ${ids[right]} reuse the same camera axis/framing with insufficient occupancy contrast`);
- }
- }
- }
- const tokens = (text) => {
- const latin = text.match(/[a-z0-9]{3,}/g) ?? [];
- const hanRuns = text.match(/\p{Script=Han}+/gu) ?? [];
- const hanBigrams = hanRuns.flatMap((run) => Array.from(
- { length: Math.max(0, run.length - 1) },
- (_, index) => run.slice(index, index + 2),
- ));
- return new Set([...latin, ...hanBigrams]);
- };
- const similarity = (left, right) => {
- const a = tokens(left);
- const b = tokens(right);
- const intersection = [...a].filter((item) => b.has(item)).length;
- const union = new Set([...a, ...b]).size;
- return union ? intersection / union : 0;
- };
- for (let left = 0; left < creativeBodies.length; left += 1) {
- for (let right = left + 1; right < creativeBodies.length; right += 1) {
- const score = similarity(creativeBodies[left], creativeBodies[right]);
- if (score > 0.72) errors.push(`creative prompts ${ids[left]} and ${ids[right]} are too similar (${score.toFixed(2)})`);
- }
- }
- }
- const secretKey = /(token|api.?key|authorization|password|secret)/i;
- const scan = (value, path = "plan") => {
- if (!value || typeof value !== "object") return;
- for (const [key, child] of Object.entries(value)) {
- if (secretKey.test(key)) errors.push(`${path}.${key} must not contain credentials`);
- scan(child, `${path}.${key}`);
- }
- };
- scan(plan);
- if (errors.length) {
- console.error(JSON.stringify({ valid: false, errors }, null, 2));
- process.exit(1);
- }
- console.log(JSON.stringify({ valid: true, run_id: plan.run_id, images: plan.images.length }, null, 2));
|