minimal-agent 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/package.json +34 -1
- package/plugins/cron-runner/.claude-plugin/plugin.json +5 -0
- package/plugins/cron-runner/commands/cron.md +26 -0
- package/plugins/cron-runner/commands/crons.md +7 -0
- package/plugins/cron-runner/plugin.js +34 -0
- package/plugins/cron-runner/src/commands.js +348 -0
- package/plugins/cron-runner/src/cronTime.js +148 -0
- package/plugins/cron-runner/src/lock.js +68 -0
- package/plugins/cron-runner/src/runs.js +89 -0
- package/plugins/cron-runner/src/scheduleEngine.js +72 -0
- package/plugins/cron-runner/src/scheduler.js +355 -0
- package/plugins/cron-runner/src/schema.js +140 -0
- package/plugins/cron-runner/src/service.js +167 -0
- package/plugins/cron-runner/src/store.js +298 -0
- package/plugins/cron-runner/src/types.js +1 -0
- package/plugins/workflow-runner/commands/workflow.md +4 -4
- package/plugins/workflow-runner/commands/workflows.md +1 -1
- package/plugins/workflow-runner/plugin.js +3 -1
- package/plugins/workflow-runner/src/agentCommon.js +249 -51
- package/plugins/workflow-runner/src/dslManifest.js +72 -0
- package/plugins/workflow-runner/src/expressions.js +198 -15
- package/plugins/workflow-runner/src/index.js +40 -8
- package/plugins/workflow-runner/src/jsonPayload.js +1 -1
- package/plugins/workflow-runner/src/loader.js +286 -72
- package/plugins/workflow-runner/src/multimodal.js +37 -0
- package/plugins/workflow-runner/src/recovery.js +49 -39
- package/plugins/workflow-runner/src/runner.js +802 -287
- package/plugins/workflow-runner/src/stepExecutors/agent.js +157 -39
- package/plugins/workflow-runner/src/stepExecutors/assert.js +16 -3
- package/plugins/workflow-runner/src/stepExecutors/llm.js +60 -111
- package/plugins/workflow-runner/src/stepExecutors/skill.js +47 -47
- package/plugins/workflow-runner/src/stepExecutors/tool.js +76 -10
- package/plugins/workflow-runner/src/stepExecutors/verify.js +27 -10
- package/plugins/workflow-runner/src/taskIdentity.js +296 -0
- package/plugins/workflow-runner/src/types.js +19 -9
- package/plugins/workflow-runner/src/workflowState.js +516 -33
- package/skills/adversarial-review/SKILL.md +0 -1
- package/skills/ai-image/SKILL.md +89 -0
- package/skills/ai-image/scripts/generate.mjs +497 -0
- package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
- package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
- package/skills/ai-image/scripts/generate_test.mjs +74 -0
- package/skills/browser-automation/SKILL.md +47 -0
- package/skills/docx/SKILL.md +25 -3
- package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
- package/skills/docx/scripts/opc_check.py +253 -0
- package/skills/image-gen-openrouter/SKILL.md +10 -2
- package/skills/image-stats/SKILL.md +65 -0
- package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/image_stats.py +368 -0
- package/skills/image-stats/scripts/image_stats_test.py +181 -0
- package/skills/pptx/SKILL.md +123 -95
- package/skills/pptx/design.md +82 -0
- package/skills/pptx/examples/README.md +29 -0
- package/skills/pptx/examples/hybrid_example.mjs +107 -0
- package/skills/pptx/examples/hybrid_example.pptx +0 -0
- package/skills/pptx/pptxgenjs.md +41 -0
- package/skills/pptx/scripts/deck_helpers.mjs +224 -0
- package/skills/pptx/scripts/layout_lint.py +293 -0
- package/skills/pptx/scripts/layout_lint_test.py +160 -0
- package/skills/pptx/scripts/opc_check.py +339 -0
- package/skills/pptx/scripts/svg_fallback.mjs +145 -0
- package/skills/pptx/svg.md +67 -0
- package/skills/tts-zh/SKILL.md +19 -2
- package/skills/ui-design/SKILL.md +158 -0
- package/skills/ui-design/claude_design.md +309 -0
- package/skills/ui-design/evals/evals.json +254 -0
- package/skills/ui-design/references/aesthetic-direction.md +83 -0
- package/skills/ui-design/references/claude-design-protocol.md +36 -0
- package/skills/ui-design/references/design-context.md +53 -0
- package/skills/ui-design/references/design-execution-playbook.md +82 -0
- package/skills/ui-design/references/export-pptx.md +124 -0
- package/skills/ui-design/references/layout-composition.md +654 -0
- package/skills/ui-design/references/preview-feedback.md +37 -0
- package/skills/ui-design/references/source-image-layout.md +189 -0
- package/skills/ui-design/references/starter-components.md +78 -0
- package/skills/ui-design/references/vi-aegis-vector.md +144 -0
- package/skills/ui-design/references/vi-amber-prism.md +133 -0
- package/skills/ui-design/references/vi-aqua-glass.md +200 -0
- package/skills/ui-design/references/vi-comfort-vision.md +147 -0
- package/skills/ui-design/references/vi-ember-prism.md +140 -0
- package/skills/ui-design/references/vi-nebula-console.md +151 -0
- package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
- package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
- package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
- package/skills/ui-design/references/vi-precision-horizon.md +215 -0
- package/skills/ui-design/references/vi-solar-relay.md +130 -0
- package/skills/ui-design/references/vi-template-workflow.md +48 -0
- package/skills/ui-design/references/vi-verdant-grid.md +141 -0
- package/skills/ui-design/references/vi-violet-meridian.md +172 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/assemble_material.py +217 -0
- package/skills/ui-design/scripts/assemble_material_test.py +149 -0
- package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
- package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
- package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
- package/skills/ui-design/scripts/check_language_policy.py +351 -0
- package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
- package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
- package/skills/ui-design/scripts/deck_pages_check.py +158 -0
- package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
- package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
- package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
- package/skills/ui-design/scripts/design_budget_check.py +271 -0
- package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
- package/skills/ui-design/scripts/design_repair_hints.py +162 -0
- package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
- package/skills/ui-design/scripts/design_style_check.py +397 -0
- package/skills/ui-design/scripts/design_style_check_test.py +233 -0
- package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
- package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
- package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
- package/skills/ui-design/scripts/extract_source_assets.py +607 -0
- package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
- package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
- package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
- package/skills/ui-design/scripts/image_plan_lint.py +571 -0
- package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
- package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
- package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
- package/skills/ui-design/scripts/layout_balance.py +881 -0
- package/skills/ui-design/scripts/layout_balance_test.py +927 -0
- package/skills/ui-design/scripts/normalize_design_json.py +272 -0
- package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
- package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
- package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
- package/skills/ui-design/scripts/resolve_source_input.py +270 -0
- package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
- package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
- package/skills/ui-design/scripts/source_usage_check.py +581 -0
- package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
- package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
- package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
- package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
- package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
- package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
- package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
- package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
- package/skills/ui-design/tests/e2e/rubric.json +54 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
- package/skills/video-compose/SKILL.md +8 -1
- package/skills/web-crawler/SKILL.md +70 -0
- package/skills/web-crawler/scripts/crawl.mjs +538 -0
- package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
- package/skills/xlsx/SKILL.md +12 -0
- package/skills/xlsx/scripts/opc_check.py +253 -0
- package/src/advisorState.js +22 -0
- package/src/askBridge.js +97 -3
- package/src/bootstrap/workingDir.js +12 -1
- package/src/cli/args.js +17 -1
- package/src/cli/configMenu.js +236 -0
- package/src/cli/configWizard.js +7 -5
- package/src/cli/print.js +37 -4
- package/src/cli/streamJson.js +77 -24
- package/src/cli/wizards/advisorWizard.js +147 -0
- package/src/cli/wizards/fieldForm.js +149 -0
- package/src/cli/wizards/imageWizard.js +60 -0
- package/src/cli/wizards/openRouterWizard.js +23 -0
- package/src/cli/wizards/providerPresets.js +95 -0
- package/src/cli/wizards/providerWizard.js +219 -0
- package/src/cli/wizards/searchWizard.js +23 -0
- package/src/cli/wizards/ttsWizard.js +34 -0
- package/src/config/configFile.js +441 -83
- package/src/config/configService.js +446 -0
- package/src/config.js +24 -64
- package/src/context/agentHome.js +9 -0
- package/src/context/archive.js +7 -2
- package/src/context/compact.js +8 -5
- package/src/context/contextLock.js +81 -0
- package/src/context/persistContext.js +30 -11
- package/src/context/reactiveCompact.js +4 -2
- package/src/context/recentDirs.js +21 -61
- package/src/context/sessionPath.js +97 -23
- package/src/context/sessionRegistry.js +303 -24
- package/src/context/tokenCounter.js +18 -0
- package/src/context/workspaceIndex.js +731 -0
- package/src/infra/verification.js +28 -15
- package/src/llm/client.js +394 -159
- package/src/llm/providers/anthropicFormat.js +241 -0
- package/src/llm/providers/openaiFormat.js +190 -0
- package/src/loop.js +219 -32
- package/src/main.js +61 -11
- package/src/messageQueue.js +97 -0
- package/src/multimodal.js +203 -0
- package/src/plugin-sdk.js +15 -0
- package/src/plugins/commandRouter.js +3 -0
- package/src/plugins/hookEngine.js +5 -1
- package/src/plugins/pluginLifecycle.js +28 -0
- package/src/plugins/pluginLoader.js +2 -1
- package/src/plugins/pluginRunner.js +49 -21
- package/src/plugins/transcript.js +4 -3
- package/src/prompts/pluginList.js +5 -5
- package/src/prompts/projectInstructions.js +3 -3
- package/src/prompts/promptAssembly.js +133 -144
- package/src/prompts/skillList.js +12 -22
- package/src/prompts/system.js +0 -1
- package/src/session/runAgentTurn.js +70 -0
- package/src/session/runTurn.js +41 -6
- package/src/toolGate.js +17 -0
- package/src/tools/advisor/advisor.js +379 -0
- package/src/tools/agent/agent.js +555 -0
- package/src/tools/agent/agentTypes.js +59 -0
- package/src/tools/agent/coordinator.js +93 -0
- package/src/tools/agent/worktree.js +270 -0
- package/src/tools/ask/ask.js +7 -2
- package/src/tools/bash/bash.js +112 -18
- package/src/tools/download/download.js +144 -0
- package/src/tools/edit/edit.js +1 -0
- package/src/tools/edit/multi-edit.js +1 -0
- package/src/tools/genimage/genimage.js +242 -0
- package/src/tools/genspeech/genspeech.js +67 -0
- package/src/tools/glob/glob.js +1 -0
- package/src/tools/grep/grep.js +3 -0
- package/src/tools/grep/rgPath.js +2 -0
- package/src/tools/index.js +25 -2
- package/src/tools/read/read.js +1 -0
- package/src/tools/shared/killTree.js +52 -0
- package/src/tools/toolProgressBridge.js +48 -0
- package/src/tools/webbrowser/agentBrowserPath.js +258 -0
- package/src/tools/webbrowser/browser.js +59 -42
- package/src/tools/webbrowser/browserControl.js +118 -0
- package/src/tools/webbrowser/browserDaemon.js +97 -0
- package/src/tools/webbrowser/browserService.js +27 -0
- package/src/tools/webbrowser/browserStream.js +119 -0
- package/src/tools/webbrowser/webbrowser.js +699 -130
- package/src/tools/webfetch/webfetch.js +1 -0
- package/src/tools/websearch/websearch.js +134 -51
- package/src/tools/write/write.js +1 -0
- package/src/ui/App.js +249 -22
- package/src/ui/AskQuestion.js +29 -10
- package/src/ui/InputBox.js +104 -12
- package/src/ui/LiveArea.js +112 -19
- package/src/ui/MessageList.js +100 -14
- package/src/ui/ModelPicker.js +56 -0
- package/src/ui/QueuedMessagesBanner.js +14 -0
- package/src/ui/Root.js +18 -10
- package/src/ui/SessionPicker.js +102 -0
- package/src/ui/StatusLine.js +13 -4
- package/src/ui/WorkspacePicker.js +55 -0
- package/src/ui/figures.js +44 -0
- package/src/ui/hooks/askSlot.js +82 -0
- package/src/ui/hooks/useChat.js +358 -64
- package/src/ui/hooks/usePasteHandler.js +11 -1
- package/src/ui/hooks/useTextBuffer.js +1 -3
- package/src/ui/liveViewport.js +5 -1
- package/src/ui/markdown.js +206 -0
- package/src/ui/textBuffer.js +0 -7
- package/src/ui/theme.js +32 -0
- package/src/utils/debugLog.js +0 -4
- package/src/utils/resourcePaths.js +2 -2
- package/src/utils/stripThink.js +50 -0
- package/src/web/browserBridge.js +447 -0
- package/src/web/protocol.js +19 -0
- package/workflows/adaptive-research.yaml +13 -5
- package/workflows/book-review-short.yaml +36 -10
- package/workflows/e2e-write-greet.yaml +6 -2
- package/workflows/illustrate.yaml +227 -0
- package/workflows/plan.yaml +460 -179
- package/workflows/ppt.yaml +1659 -0
- package/workflows/ralph-loop.yaml +199 -0
- package/workflows/schema.json +81 -23
- package/workflows/webppt.yaml +496 -0
- package/workflows/word.yaml +710 -0
- package/workflows/youtube-shorts.yaml +46 -15
- package/MINIMAL-TRACE.md +0 -77
- package/plugins/HOW-TO-WRITE-A-PLUGIN.md +0 -186
- package/plugins/ralph-wiggum/.claude-plugin/plugin.json +0 -9
- package/plugins/ralph-wiggum/README.md +0 -197
- package/plugins/ralph-wiggum/commands/cancel-ralph.md +0 -18
- package/plugins/ralph-wiggum/commands/help.md +0 -137
- package/plugins/ralph-wiggum/commands/ralph-loop.md +0 -85
- package/plugins/ralph-wiggum/hooks/hooks.json +0 -15
- package/plugins/ralph-wiggum/hooks/stop-hook.sh +0 -191
- package/plugins/ralph-wiggum/plugin.js +0 -280
- package/plugins/ralph-wiggum/src/goalState.js +0 -258
- package/plugins/ralph-wiggum/src/sentinels.js +0 -76
- package/plugins/ralph-wiggum/src/stopHookRunner.js +0 -104
- package/plugins/ralph-wiggum/src/verificationGate.js +0 -202
- package/plugins/workflow-runner/src/miniReAct.js +0 -223
- package/plugins/workflow-runner/src/voteHelpers.js +0 -110
- package/skills/config/SKILL.md +0 -123
- package/skills/frontend-design/LICENSE.txt +0 -177
- package/skills/frontend-design/SKILL.md +0 -42
- package/src/ui/ProgressPanel.js +0 -54
- package/src/ui/StreamingBlock.js +0 -11
- package/src/ui/ToolStatus.js +0 -11
|
@@ -0,0 +1,1136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
plan_source_layout_test.py —— plan_source_layout.py 单测。
|
|
5
|
+
|
|
6
|
+
锁定确定性判定(治「客户图被用烂」根因的核心逻辑):
|
|
7
|
+
- ① 全图生图政策:**所有客户图(除无 vision 盲态)一律 enhance_required=true**,不再直接用原图。
|
|
8
|
+
合格+背景图 → source_enhanced_bg(16:9);合格+非背景 → source_enhanced_hero(按朝向定比例);低清/坏比例同理。
|
|
9
|
+
信息密集图(截图/图表)→ direct_contain,**照样 img2img 但走忠实保真提示词**(dense,best-effort 有重绘风险)。
|
|
10
|
+
无识图(无多模态)盲态 = 唯一保守例外(low_res→small_evidence,enhance=false)。
|
|
11
|
+
- ② _background_kind 识图分流:real_scene / flat_base / unknown,联动 enhance_style(real_scene→hero / flat_base+离散主体→cutout)。
|
|
12
|
+
- ③ 三态提示词(bg/hero/cutout)统一含高清质量词(high resolution / sharp / clean lighting…)。
|
|
13
|
+
- 文字深浅按 stats.brightness 定(亮底深墨字 / 暗底白字)。
|
|
14
|
+
- ensure-img-plan:把图生图条目 union 进 img_plan.json,按 file 去重不重复注入。
|
|
15
|
+
- 无客户图 → 空规划、enhance_count=0。
|
|
16
|
+
依赖:标准库 + 同目录 source_usage_check。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import unittest
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
import plan_source_layout as psl # noqa: E402
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _img(file, w, h, *, orient=None, vision=None, stats=None, section=None, aspect_ratio=None,
|
|
30
|
+
near_text=None, origin=None):
|
|
31
|
+
d = {"file": file, "w": w, "h": h}
|
|
32
|
+
if orient:
|
|
33
|
+
d["orient"] = orient
|
|
34
|
+
if aspect_ratio is not None:
|
|
35
|
+
d["aspect_ratio"] = aspect_ratio
|
|
36
|
+
elif h:
|
|
37
|
+
d["aspect_ratio"] = round(w / h, 3)
|
|
38
|
+
if vision is not None:
|
|
39
|
+
d["vision"] = vision
|
|
40
|
+
if stats is not None:
|
|
41
|
+
d["stats"] = stats
|
|
42
|
+
if section:
|
|
43
|
+
d["section"] = section
|
|
44
|
+
if near_text:
|
|
45
|
+
d["near_text"] = near_text
|
|
46
|
+
if origin:
|
|
47
|
+
d["origin"] = origin
|
|
48
|
+
return d
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _vision(desc="一台工业相机产品照", *, bg=False, zone=None, elements=None,
|
|
52
|
+
content_type=None, embedded_text=None):
|
|
53
|
+
v = {"description": desc, "suitable_as_background": bg}
|
|
54
|
+
if zone:
|
|
55
|
+
v["recommended_text_zone"] = zone
|
|
56
|
+
if elements:
|
|
57
|
+
v["key_elements"] = elements
|
|
58
|
+
if content_type is not None:
|
|
59
|
+
v["content_type"] = content_type
|
|
60
|
+
if embedded_text is not None:
|
|
61
|
+
v["embedded_text"] = embedded_text
|
|
62
|
+
return v
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestPlanOne(unittest.TestCase):
|
|
66
|
+
def test_low_res_background_suitable_needs_img2img_bg(self):
|
|
67
|
+
e = psl.plan_one(_img("src_1.png", 800, 450, vision=_vision("开阔的山景风光照", bg=True)))
|
|
68
|
+
self.assertEqual(e["quality"], "low_res")
|
|
69
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
70
|
+
self.assertEqual(e["image_role"], "background")
|
|
71
|
+
self.assertEqual(e["target_layer"], "background")
|
|
72
|
+
self.assertEqual(e["text_policy"], "avoid_new_text")
|
|
73
|
+
self.assertTrue(e["enhance_required"])
|
|
74
|
+
self.assertEqual(e["enhance_target_aspect"], "16:9")
|
|
75
|
+
self.assertEqual(e["enhanced_file"], "enhanced_src_1.png")
|
|
76
|
+
self.assertIn("no text", e["enhance_prompt"])
|
|
77
|
+
self.assertIn("16:9", e["enhance_prompt"])
|
|
78
|
+
# 用户原话三要素:保留原图内容 + 高清重绘 + 缺失区域智能补全(outpaint)
|
|
79
|
+
self.assertIn("preserve", e["enhance_prompt"])
|
|
80
|
+
self.assertIn("high resolution", e["enhance_prompt"])
|
|
81
|
+
self.assertIn("outpaint", e["enhance_prompt"])
|
|
82
|
+
self.assertIn("背景主导", e["recommended_templates"])
|
|
83
|
+
|
|
84
|
+
def test_low_res_portrait_product_needs_img2img_hero_4x5(self):
|
|
85
|
+
e = psl.plan_one(_img("src_2.png", 600, 900, vision=_vision("竖拍的手持设备产品照", bg=False)))
|
|
86
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
87
|
+
self.assertTrue(e["enhance_required"])
|
|
88
|
+
self.assertEqual(e["enhance_target_aspect"], "4:5")
|
|
89
|
+
self.assertEqual(e["orient"], "portrait")
|
|
90
|
+
self.assertIn("左右产品剧场", e["recommended_templates"])
|
|
91
|
+
|
|
92
|
+
def test_high_res_background_suitable_now_img2img(self):
|
|
93
|
+
# ① 全图生图:高清合格背景图也不再直接铺,改图生图重绘成高清干净全幅背景。
|
|
94
|
+
e = psl.plan_one(_img("src_3.png", 1920, 1080, vision=_vision("辽阔的数据中心机房", bg=True)))
|
|
95
|
+
self.assertEqual(e["quality"], "ok")
|
|
96
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
97
|
+
self.assertTrue(e["enhance_required"])
|
|
98
|
+
self.assertEqual(e["enhance_style"], "bg")
|
|
99
|
+
self.assertEqual(e["enhance_target_aspect"], "16:9")
|
|
100
|
+
self.assertEqual(e["enhanced_file"], "enhanced_src_3.png")
|
|
101
|
+
self.assertIn("enhance_prompt", e)
|
|
102
|
+
|
|
103
|
+
def test_dense_info_screenshot_img2img_faithful(self):
|
|
104
|
+
# 用户明确要求:dense(截图/图表)照样 img2img,但走「忠实保留版面/数据」提示词(best-effort,有重绘风险)。
|
|
105
|
+
e = psl.plan_one(
|
|
106
|
+
_img("src_4.png", 700, 480, vision=_vision("产品后台仪表盘界面截图", bg=False, elements=["图表", "表格"]))
|
|
107
|
+
)
|
|
108
|
+
self.assertTrue(e["is_dense_info"])
|
|
109
|
+
self.assertEqual(e["usage_intent"], "direct_contain")
|
|
110
|
+
self.assertTrue(e["enhance_required"])
|
|
111
|
+
self.assertEqual(e["enhance_style"], "dense")
|
|
112
|
+
self.assertEqual(e["image_role"], "faithful_document")
|
|
113
|
+
self.assertEqual(e["text_policy"], "preserve_data_text")
|
|
114
|
+
self.assertEqual(e["target_layer"], "evidence")
|
|
115
|
+
self.assertEqual(e["enhanced_file"], "enhanced_src_4.png")
|
|
116
|
+
self.assertIn("图像证据卡", e["recommended_templates"])
|
|
117
|
+
p = e["enhance_prompt"].lower()
|
|
118
|
+
self.assertIn("layout", p) # 忠实保留版面
|
|
119
|
+
self.assertIn("high resolution", p) # 高清质量词
|
|
120
|
+
self.assertNotIn("no text", p) # dense 含合法数据标签/文字,绝不去字
|
|
121
|
+
|
|
122
|
+
def test_no_vision_blind_conservative(self):
|
|
123
|
+
e = psl.plan_one(_img("src_5.png", 700, 500)) # 无 vision
|
|
124
|
+
self.assertFalse(e["has_vision"])
|
|
125
|
+
self.assertFalse(e["enhance_required"]) # 唯一保守例外:盲态不冒险图生图
|
|
126
|
+
self.assertEqual(e["usage_intent"], "small_evidence")
|
|
127
|
+
|
|
128
|
+
def test_ok_photo_non_background_now_img2img_hero(self):
|
|
129
|
+
# ① 全图生图:高清合格非背景图也不再直接 contain,改图生图重绘成高清主视觉。
|
|
130
|
+
e = psl.plan_one(_img("src_6.png", 1600, 1200, vision=_vision("一块精密电路板特写", bg=False)))
|
|
131
|
+
self.assertEqual(e["quality"], "ok")
|
|
132
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
133
|
+
self.assertTrue(e["enhance_required"])
|
|
134
|
+
self.assertEqual(e["target_layer"], "foreground")
|
|
135
|
+
self.assertIn("enhance_prompt", e)
|
|
136
|
+
|
|
137
|
+
def test_flat_base_discrete_subject_cutout_metadata(self):
|
|
138
|
+
e = psl.plan_one(
|
|
139
|
+
_img(
|
|
140
|
+
"src_cut.png",
|
|
141
|
+
1200,
|
|
142
|
+
900,
|
|
143
|
+
vision=_vision("白底上的独立设备产品特写", bg=False, elements=["设备", "白底"]),
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
self.assertEqual(e["enhance_style"], "cutout")
|
|
147
|
+
self.assertEqual(e["image_role"], "foreground_cutout")
|
|
148
|
+
self.assertEqual(e["target_layer"], "foreground")
|
|
149
|
+
self.assertIn("transparent", e["enhance_prompt"].lower())
|
|
150
|
+
|
|
151
|
+
def test_text_color_from_brightness(self):
|
|
152
|
+
bright = psl.plan_one(
|
|
153
|
+
_img("src_7.png", 1920, 1080, vision=_vision("明亮的白色产品照", bg=True), stats={"brightness": {"mean": 210}})
|
|
154
|
+
)
|
|
155
|
+
dark = psl.plan_one(
|
|
156
|
+
_img("src_8.png", 1920, 1080, vision=_vision("夜景城市天际线", bg=True), stats={"brightness": {"mean": 40}})
|
|
157
|
+
)
|
|
158
|
+
self.assertEqual(bright["text_color"], "dark")
|
|
159
|
+
self.assertEqual(dark["text_color"], "light")
|
|
160
|
+
|
|
161
|
+
def test_text_zone_from_vision(self):
|
|
162
|
+
e = psl.plan_one(_img("src_9.png", 800, 600, vision=_vision("产品图", bg=True, zone="left")))
|
|
163
|
+
self.assertEqual(e["text_zone"], "left")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestDisplayPolicy(unittest.TestCase):
|
|
167
|
+
"""显示档位 + 尺寸下限 + 缩放上限(治「客户图缩成角落小图看不清 / 低清原图放大糊」)。"""
|
|
168
|
+
|
|
169
|
+
def test_background_role_full_canvas(self):
|
|
170
|
+
e = psl.plan_one(_img("b.png", 1920, 1080, vision=_vision("辽阔机房", bg=True)))
|
|
171
|
+
self.assertEqual(e["display_role"], "background")
|
|
172
|
+
self.assertEqual(e["min_canvas_fraction"], 1.0)
|
|
173
|
+
self.assertEqual(e["max_upscale"], 1.0)
|
|
174
|
+
|
|
175
|
+
def test_hero_role_min_30pct(self):
|
|
176
|
+
# 高清非背景照 → 现在 source_enhanced_hero → foreground_hero,槽位至少占画布 30%
|
|
177
|
+
e = psl.plan_one(_img("h.png", 1600, 1200, vision=_vision("电路板特写", bg=False)))
|
|
178
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
179
|
+
self.assertEqual(e["display_role"], "foreground_hero")
|
|
180
|
+
self.assertGreaterEqual(e["min_canvas_fraction"], 0.30)
|
|
181
|
+
|
|
182
|
+
def test_enhanced_hero_is_foreground_hero(self):
|
|
183
|
+
e = psl.plan_one(_img("p.png", 600, 900, vision=_vision("竖拍设备照", bg=False)))
|
|
184
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
185
|
+
self.assertEqual(e["display_role"], "foreground_hero")
|
|
186
|
+
|
|
187
|
+
def test_small_evidence_still_readable_min(self):
|
|
188
|
+
# 低清无识图 → small_evidence → evidence_card,但仍须 ≥12% 画布(非角落缩略图)
|
|
189
|
+
e = psl.plan_one(_img("s.png", 700, 500))
|
|
190
|
+
self.assertEqual(e["usage_intent"], "small_evidence")
|
|
191
|
+
self.assertEqual(e["display_role"], "evidence_card")
|
|
192
|
+
self.assertGreaterEqual(e["min_canvas_fraction"], 0.12)
|
|
193
|
+
|
|
194
|
+
def test_max_upscale_always_one(self):
|
|
195
|
+
for img in (
|
|
196
|
+
_img("a.png", 1920, 1080, vision=_vision("机房", bg=True)),
|
|
197
|
+
_img("b.png", 600, 900, vision=_vision("竖拍设备", bg=False)),
|
|
198
|
+
_img("c.png", 700, 500),
|
|
199
|
+
_img("d.png", 700, 480, vision=_vision("仪表盘截图", bg=False, elements=["图表"])),
|
|
200
|
+
):
|
|
201
|
+
self.assertEqual(psl.plan_one(img)["max_upscale"], 1.0)
|
|
202
|
+
|
|
203
|
+
def test_gallery_role_via_build(self):
|
|
204
|
+
d = tempfile.mkdtemp(prefix="srclayout-role-")
|
|
205
|
+
with open(os.path.join(d, "material.json"), "w", encoding="utf-8") as f:
|
|
206
|
+
json.dump(
|
|
207
|
+
{
|
|
208
|
+
"has_source": True,
|
|
209
|
+
"images": [
|
|
210
|
+
_img(f"g{i}.png", 1500, 1000, vision=_vision(f"现场{i}", bg=False), section="案例")
|
|
211
|
+
for i in range(3)
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
f,
|
|
215
|
+
ensure_ascii=False,
|
|
216
|
+
)
|
|
217
|
+
plan = psl.build_plan(d)
|
|
218
|
+
self.assertTrue(all(e["display_role"] == "gallery" for e in plan["images"]))
|
|
219
|
+
self.assertTrue(all(e["min_canvas_fraction"] == 0.10 for e in plan["images"]))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestLayoutConstraint(unittest.TestCase):
|
|
223
|
+
"""版式硬约束 layout_constraint(设计步照读、不自行决定 render;治「有底图还套表格/卡墙划区」)。"""
|
|
224
|
+
|
|
225
|
+
def test_high_res_bg_is_fusion_no_cards(self):
|
|
226
|
+
# 全图生图后高清背景图 → source_enhanced_bg,版式仍是编辑式融合(禁卡墙)。
|
|
227
|
+
e = psl.plan_one(_img("b.png", 1920, 1080, vision=_vision("辽阔机房", bg=True)))
|
|
228
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
229
|
+
self.assertEqual(e["layout_constraint"], "fusion_no_cards")
|
|
230
|
+
|
|
231
|
+
def test_enhanced_bg_is_fusion_no_cards(self):
|
|
232
|
+
e = psl.plan_one(_img("eb.png", 800, 450, vision=_vision("山景", bg=True)))
|
|
233
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
234
|
+
self.assertEqual(e["layout_constraint"], "fusion_no_cards")
|
|
235
|
+
|
|
236
|
+
def test_enhanced_hero_is_fusion_no_cards(self):
|
|
237
|
+
e = psl.plan_one(_img("eh.png", 600, 900, vision=_vision("竖拍设备照", bg=False)))
|
|
238
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
239
|
+
self.assertEqual(e["layout_constraint"], "fusion_no_cards")
|
|
240
|
+
|
|
241
|
+
def test_direct_contain_is_image_text_mixed(self):
|
|
242
|
+
# direct_contain 现在由「无 vision 合格图」产出(不增强)→ image_text_mixed。
|
|
243
|
+
e = psl.plan_one(_img("dc.png", 1600, 1200)) # 无 vision、合格
|
|
244
|
+
self.assertEqual(e["usage_intent"], "direct_contain")
|
|
245
|
+
self.assertFalse(e["enhance_required"])
|
|
246
|
+
self.assertEqual(e["layout_constraint"], "image_text_mixed")
|
|
247
|
+
|
|
248
|
+
def test_small_evidence_is_evidence_ok(self):
|
|
249
|
+
e = psl.plan_one(_img("se.png", 700, 500)) # 低清无识图
|
|
250
|
+
self.assertEqual(e["usage_intent"], "small_evidence")
|
|
251
|
+
self.assertEqual(e["layout_constraint"], "evidence_ok")
|
|
252
|
+
|
|
253
|
+
def test_dense_info_is_image_text_mixed(self):
|
|
254
|
+
# 信息密集图 direct_contain → image_text_mixed(不背景化、不堆卡墙于图上)
|
|
255
|
+
e = psl.plan_one(
|
|
256
|
+
_img("d.png", 700, 480, vision=_vision("仪表盘截图", bg=False, elements=["图表", "表格"]))
|
|
257
|
+
)
|
|
258
|
+
self.assertEqual(e["usage_intent"], "direct_contain")
|
|
259
|
+
self.assertEqual(e["layout_constraint"], "image_text_mixed")
|
|
260
|
+
|
|
261
|
+
def test_gallery_constraint_via_build(self):
|
|
262
|
+
d = tempfile.mkdtemp(prefix="srclayout-lc-")
|
|
263
|
+
with open(os.path.join(d, "material.json"), "w", encoding="utf-8") as f:
|
|
264
|
+
json.dump(
|
|
265
|
+
{
|
|
266
|
+
"has_source": True,
|
|
267
|
+
"images": [
|
|
268
|
+
_img(f"g{i}.png", 1500, 1000, vision=_vision(f"现场{i}", bg=False), section="案例")
|
|
269
|
+
for i in range(3)
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
f,
|
|
273
|
+
ensure_ascii=False,
|
|
274
|
+
)
|
|
275
|
+
plan = psl.build_plan(d)
|
|
276
|
+
self.assertTrue(all(e["usage_intent"] == "gallery" for e in plan["images"]))
|
|
277
|
+
self.assertTrue(all(e["layout_constraint"] == "evidence_ok" for e in plan["images"]))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class TestBuildAndEnsure(unittest.TestCase):
|
|
281
|
+
def setUp(self):
|
|
282
|
+
self.d = tempfile.mkdtemp(prefix="srclayout-")
|
|
283
|
+
|
|
284
|
+
def _material(self, images):
|
|
285
|
+
with open(os.path.join(self.d, "material.json"), "w", encoding="utf-8") as f:
|
|
286
|
+
json.dump({"has_source": True, "images": images}, f, ensure_ascii=False)
|
|
287
|
+
|
|
288
|
+
def test_build_plan_counts_and_files(self):
|
|
289
|
+
self._material(
|
|
290
|
+
[
|
|
291
|
+
_img("src_1.png", 800, 450, vision=_vision("山景", bg=True)), # 低清 bg → enhance
|
|
292
|
+
_img("src_2.png", 1920, 1080, vision=_vision("机房", bg=True)), # ① 高清 bg 现也 enhance
|
|
293
|
+
]
|
|
294
|
+
)
|
|
295
|
+
plan = psl.build_plan(self.d)
|
|
296
|
+
self.assertEqual(plan["image_count"], 2)
|
|
297
|
+
self.assertEqual(plan["enhance_count"], 2) # ① 全图生图:两张背景图都需重绘
|
|
298
|
+
self.assertEqual(plan["enhance_entries"][0]["mode"], "image-to-image")
|
|
299
|
+
self.assertEqual(plan["enhance_entries"][0]["source_file"], "src_1.png")
|
|
300
|
+
self.assertEqual(plan["enhance_entries"][0]["file"], "enhanced_src_1.png")
|
|
301
|
+
# 落盘 md/json
|
|
302
|
+
psl.write_plan_md(self.d, plan)
|
|
303
|
+
with open(os.path.join(self.d, "source_layout_plan.json"), "w", encoding="utf-8") as f:
|
|
304
|
+
json.dump(plan, f, ensure_ascii=False)
|
|
305
|
+
self.assertTrue(os.path.isfile(os.path.join(self.d, "source_layout_plan.md")))
|
|
306
|
+
|
|
307
|
+
def test_no_images_empty_plan(self):
|
|
308
|
+
self._material([])
|
|
309
|
+
plan = psl.build_plan(self.d)
|
|
310
|
+
self.assertEqual(plan["image_count"], 0)
|
|
311
|
+
self.assertEqual(plan["enhance_count"], 0)
|
|
312
|
+
self.assertEqual(plan["enhance_entries"], [])
|
|
313
|
+
|
|
314
|
+
def test_ensure_img_plan_injects_and_dedups(self):
|
|
315
|
+
self._material([_img("src_1.png", 800, 450, vision=_vision("山景", bg=True))])
|
|
316
|
+
plan = psl.build_plan(self.d)
|
|
317
|
+
with open(os.path.join(self.d, "source_layout_plan.json"), "w", encoding="utf-8") as f:
|
|
318
|
+
json.dump(plan, f, ensure_ascii=False)
|
|
319
|
+
# 预置一个纯文生图条目,注入不应覆盖它,且图生图条目应被加入。
|
|
320
|
+
with open(os.path.join(self.d, "img_plan.json"), "w", encoding="utf-8") as f:
|
|
321
|
+
json.dump([{"file": "img_1.png", "mode": "text-to-image", "prompt": "a mountain, no text"}], f)
|
|
322
|
+
rc = psl.ensure_img_plan(self.d)
|
|
323
|
+
self.assertEqual(rc, 0)
|
|
324
|
+
with open(os.path.join(self.d, "img_plan.json"), encoding="utf-8") as f:
|
|
325
|
+
ip = json.load(f)
|
|
326
|
+
files = [e["file"] for e in ip]
|
|
327
|
+
self.assertIn("img_1.png", files)
|
|
328
|
+
self.assertIn("enhanced_src_1.png", files)
|
|
329
|
+
self.assertEqual(len(ip), 2)
|
|
330
|
+
# 再次注入应幂等(不重复)。
|
|
331
|
+
psl.ensure_img_plan(self.d)
|
|
332
|
+
with open(os.path.join(self.d, "img_plan.json"), encoding="utf-8") as f:
|
|
333
|
+
ip2 = json.load(f)
|
|
334
|
+
self.assertEqual(len(ip2), 2)
|
|
335
|
+
|
|
336
|
+
def test_gallery_grouping_same_section(self):
|
|
337
|
+
self._material(
|
|
338
|
+
[
|
|
339
|
+
_img("src_1.png", 1500, 1000, vision=_vision("现场照A", bg=False), section="案例展示"),
|
|
340
|
+
_img("src_2.png", 1500, 1000, vision=_vision("现场照B", bg=False), section="案例展示"),
|
|
341
|
+
_img("src_3.png", 1500, 1000, vision=_vision("现场照C", bg=False), section="案例展示"),
|
|
342
|
+
]
|
|
343
|
+
)
|
|
344
|
+
plan = psl.build_plan(self.d)
|
|
345
|
+
self.assertTrue(all(e["usage_intent"] == "gallery" for e in plan["images"]))
|
|
346
|
+
self.assertTrue(all("四宫格画廊" in e["recommended_templates"] for e in plan["images"]))
|
|
347
|
+
# 画廊每格照样图生图增强到高清(gallery 不再豁免)。
|
|
348
|
+
self.assertTrue(all(e["enhance_required"] for e in plan["images"]))
|
|
349
|
+
self.assertEqual(plan["enhance_count"], 3)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class TestSectionPassthrough(unittest.TestCase):
|
|
353
|
+
"""章节锚定透传(2026-07-02 张冠李戴裁决):extract 采集的 section/near_text/origin 此前在
|
|
354
|
+
plan entry 被丢弃 → 设计步只能按识图语义配页。现原样透传 + plan.md 渲染配页锚点。"""
|
|
355
|
+
|
|
356
|
+
def test_entry_carries_section_near_text_origin(self):
|
|
357
|
+
e = psl.plan_one(_img("src_1.png", 1500, 1000, vision=_vision("高空作业平台"),
|
|
358
|
+
section="三、现场作业场景", near_text="巡检人员在高空平台开展检修作业",
|
|
359
|
+
origin="doc.docx#三、现场作业场景"))
|
|
360
|
+
self.assertEqual(e["section"], "三、现场作业场景")
|
|
361
|
+
self.assertEqual(e["near_text"], "巡检人员在高空平台开展检修作业")
|
|
362
|
+
self.assertEqual(e["origin"], "doc.docx#三、现场作业场景")
|
|
363
|
+
|
|
364
|
+
def test_near_text_truncated_to_120(self):
|
|
365
|
+
e = psl.plan_one(_img("src_1.png", 1500, 1000, near_text="巡检" * 100))
|
|
366
|
+
self.assertEqual(len(e["near_text"]), 120)
|
|
367
|
+
|
|
368
|
+
def test_absent_fields_stay_absent(self):
|
|
369
|
+
e = psl.plan_one(_img("src_1.png", 1500, 1000))
|
|
370
|
+
self.assertNotIn("section", e)
|
|
371
|
+
self.assertNotIn("near_text", e)
|
|
372
|
+
self.assertNotIn("origin", e)
|
|
373
|
+
|
|
374
|
+
def test_plan_md_renders_section_anchor(self):
|
|
375
|
+
d = tempfile.mkdtemp(prefix="srclayout-sec-")
|
|
376
|
+
with open(os.path.join(d, "material.json"), "w", encoding="utf-8") as f:
|
|
377
|
+
json.dump({"has_source": True, "images": [
|
|
378
|
+
_img("src_1.png", 1500, 1000, vision=_vision("现场照"),
|
|
379
|
+
section="三、现场作业场景", near_text="巡检人员在高空平台开展检修作业")]},
|
|
380
|
+
f, ensure_ascii=False)
|
|
381
|
+
plan = psl.build_plan(d)
|
|
382
|
+
psl.write_plan_md(d, plan)
|
|
383
|
+
with open(os.path.join(d, "source_layout_plan.md"), encoding="utf-8") as f:
|
|
384
|
+
md = f.read()
|
|
385
|
+
self.assertIn("来源章节:**三、现场作业场景**", md)
|
|
386
|
+
self.assertIn("配页锚点", md)
|
|
387
|
+
self.assertIn("巡检人员在高空平台开展检修作业", md)
|
|
388
|
+
|
|
389
|
+
def test_key_message_passthrough_and_rendered(self):
|
|
390
|
+
vision = _vision("巡检效率对比图")
|
|
391
|
+
vision["content_type"] = "chart"
|
|
392
|
+
vision["key_message"] = "巡检效率提升三倍"
|
|
393
|
+
e = psl.plan_one(_img("src_1.png", 1500, 1000, vision=vision))
|
|
394
|
+
self.assertEqual(e["key_message"], "巡检效率提升三倍")
|
|
395
|
+
|
|
396
|
+
d = tempfile.mkdtemp(prefix="srclayout-km-")
|
|
397
|
+
with open(os.path.join(d, "material.json"), "w", encoding="utf-8") as f:
|
|
398
|
+
json.dump({"has_source": True, "images": [_img("src_1.png", 1500, 1000, vision=vision)]},
|
|
399
|
+
f, ensure_ascii=False)
|
|
400
|
+
plan = psl.build_plan(d)
|
|
401
|
+
psl.write_plan_md(d, plan)
|
|
402
|
+
with open(os.path.join(d, "source_layout_plan.md"), encoding="utf-8") as f:
|
|
403
|
+
md = f.read()
|
|
404
|
+
self.assertIn("巡检效率提升三倍", md)
|
|
405
|
+
self.assertIn("key_point/evidence 须呼应", md)
|
|
406
|
+
|
|
407
|
+
def test_no_key_message_field_absent(self):
|
|
408
|
+
e = psl.plan_one(_img("src_1.png", 1500, 1000, vision=_vision("普通照片")))
|
|
409
|
+
self.assertNotIn("key_message", e)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class TestEnsureAiImgPlan(unittest.TestCase):
|
|
413
|
+
"""--ensure-ai-img-plan:把 design.json 的 ai-A/ai-B 页文生图条目确定性 union 进 img_plan
|
|
414
|
+
(约定名 ai_p<page>.png),不靠设计 LLM 记得写、按 file 去重幂等。治真机「AI 背景从未进 img_plan」事故。"""
|
|
415
|
+
|
|
416
|
+
def setUp(self):
|
|
417
|
+
self.d = tempfile.mkdtemp(prefix="srclayout-ai-")
|
|
418
|
+
|
|
419
|
+
def _design(self, pages):
|
|
420
|
+
with open(os.path.join(self.d, "design.json"), "w", encoding="utf-8") as f:
|
|
421
|
+
json.dump({"pages": pages}, f, ensure_ascii=False)
|
|
422
|
+
|
|
423
|
+
def test_injects_ai_pages_with_convention_names(self):
|
|
424
|
+
self._design(
|
|
425
|
+
[
|
|
426
|
+
{"page": 1, "visual": {"route": "source-enhanced", "source_file": "src_1.png"}},
|
|
427
|
+
{"page": 2, "visual": {"route": "ai-B", "aspect": "16:9", "prompt": "a calm blue industrial backdrop, no text"}},
|
|
428
|
+
{"page": 3, "visual": {"route": "html"}},
|
|
429
|
+
{"page": 4, "visual": {"route": "ai-A", "side": "right", "aspect": "4:5", "prompt": "an engineer in a hangar, no text"}},
|
|
430
|
+
{"page": 5, "visual": {"route": "ai-B", "aspect": "16:9"}}, # 无 prompt → 跳过
|
|
431
|
+
]
|
|
432
|
+
)
|
|
433
|
+
# 预置一个客户图增强条目,注入不应覆盖它。
|
|
434
|
+
with open(os.path.join(self.d, "img_plan.json"), "w", encoding="utf-8") as f:
|
|
435
|
+
json.dump(
|
|
436
|
+
[{"file": "enhanced_src_1.png", "mode": "image-to-image", "source_file": "src_1.png", "prompt": "x"}], f
|
|
437
|
+
)
|
|
438
|
+
rc = psl.ensure_ai_img_plan(self.d)
|
|
439
|
+
self.assertEqual(rc, 0)
|
|
440
|
+
with open(os.path.join(self.d, "img_plan.json"), encoding="utf-8") as f:
|
|
441
|
+
ip = json.load(f)
|
|
442
|
+
by_file = {e["file"]: e for e in ip}
|
|
443
|
+
# ai-B p2 是整页背景 ai_p<page>.png;ai-A p4 是前景插图 ai_p<page>_fg.png。
|
|
444
|
+
self.assertIn("ai_p2.png", by_file)
|
|
445
|
+
self.assertIn("ai_p4_fg.png", by_file)
|
|
446
|
+
self.assertEqual(by_file["ai_p2.png"]["mode"], "text-to-image")
|
|
447
|
+
self.assertEqual(by_file["ai_p2.png"]["aspect"], "16:9")
|
|
448
|
+
self.assertEqual(by_file["ai_p4_fg.png"]["aspect"], "4:5")
|
|
449
|
+
self.assertEqual(by_file["ai_p4_fg.png"]["target_layer"], "foreground")
|
|
450
|
+
# html 页不生 AI 图;无 prompt 的 ai-B(p5)跳过;客户图增强条目保留。
|
|
451
|
+
self.assertNotIn("ai_p3.png", by_file)
|
|
452
|
+
self.assertNotIn("ai_p4.png", by_file)
|
|
453
|
+
self.assertNotIn("ai_p5.png", by_file)
|
|
454
|
+
self.assertIn("enhanced_src_1.png", by_file)
|
|
455
|
+
self.assertEqual(len(ip), 3)
|
|
456
|
+
# 幂等:再次注入不重复。
|
|
457
|
+
psl.ensure_ai_img_plan(self.d)
|
|
458
|
+
with open(os.path.join(self.d, "img_plan.json"), encoding="utf-8") as f:
|
|
459
|
+
ip2 = json.load(f)
|
|
460
|
+
self.assertEqual(len(ip2), 3)
|
|
461
|
+
|
|
462
|
+
def test_no_design_no_crash(self):
|
|
463
|
+
# 无 design.json → 跳过、不崩、不建 img_plan。
|
|
464
|
+
rc = psl.ensure_ai_img_plan(self.d)
|
|
465
|
+
self.assertEqual(rc, 0)
|
|
466
|
+
|
|
467
|
+
def test_creates_img_plan_when_absent(self):
|
|
468
|
+
# 纯 topic deck(无客户图、img_plan 还不存在):ai-* 仍被注入、img_plan 被创建。
|
|
469
|
+
self._design([{"page": 1, "visual": {"route": "ai-B", "aspect": "16:9", "prompt": "atmosphere, no text"}}])
|
|
470
|
+
psl.ensure_ai_img_plan(self.d)
|
|
471
|
+
with open(os.path.join(self.d, "img_plan.json"), encoding="utf-8") as f:
|
|
472
|
+
ip = json.load(f)
|
|
473
|
+
self.assertEqual([e["file"] for e in ip], ["ai_p1.png"])
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class TestEnsurePageBackgrounds(unittest.TestCase):
|
|
477
|
+
"""--ensure-page-backgrounds:为【无背景图的内容页】确定性补一条 VI 氛围文生图底图条目
|
|
478
|
+
(约定名 ai_p<page>.png),治真机「约一半内容页 route:none/html 无底图→退化白底卡片墙」。
|
|
479
|
+
在 --ensure-ai-img-plan 之后跑、按 file 去重幂等。"""
|
|
480
|
+
|
|
481
|
+
def setUp(self):
|
|
482
|
+
self.d = tempfile.mkdtemp(prefix="srclayout-pgbg-")
|
|
483
|
+
|
|
484
|
+
def _design(self, pages):
|
|
485
|
+
with open(os.path.join(self.d, "design.json"), "w", encoding="utf-8") as f:
|
|
486
|
+
json.dump({"pages": pages}, f, ensure_ascii=False)
|
|
487
|
+
|
|
488
|
+
def _img_plan(self, entries):
|
|
489
|
+
with open(os.path.join(self.d, "img_plan.json"), "w", encoding="utf-8") as f:
|
|
490
|
+
json.dump(entries, f, ensure_ascii=False)
|
|
491
|
+
|
|
492
|
+
def _read_plan(self):
|
|
493
|
+
with open(os.path.join(self.d, "img_plan.json"), encoding="utf-8") as f:
|
|
494
|
+
return json.load(f)
|
|
495
|
+
|
|
496
|
+
def test_backfills_only_bare_content_pages(self):
|
|
497
|
+
self._design(
|
|
498
|
+
[
|
|
499
|
+
# ① 客户图整页底 → 有背景 → 跳过
|
|
500
|
+
{"page": 1, "visual": {"route": "source-enhanced", "usage_intent": "source_enhanced_bg", "source_file": "src_1.png"}},
|
|
501
|
+
# ② 设计选定带提示词的 ai-B → 有背景(ensure_ai_img_plan 已注入)→ 跳过
|
|
502
|
+
{"page": 2, "visual": {"route": "ai-B", "aspect": "16:9", "prompt": "a calm industrial backdrop, no text"}},
|
|
503
|
+
# ③ 结构页 route=html(前景是 HTML 图表、没有背景图)→ 补氛围底图
|
|
504
|
+
{"page": 3, "visual": {"route": "html"}},
|
|
505
|
+
# ④ route=none(没有视觉)→ 补氛围底图
|
|
506
|
+
{"page": 4, "visual": {"route": "none"}},
|
|
507
|
+
# ⑤ 完全缺 visual → 补氛围底图
|
|
508
|
+
{"page": 5},
|
|
509
|
+
# ⑥ route=ai-B 但【无 prompt】(ensure_ai_img_plan 已跳过它、留给兜底)→ 该页 AI 底图没着落 → 补氛围底图
|
|
510
|
+
{"page": 6, "visual": {"route": "ai-B", "aspect": "16:9"}},
|
|
511
|
+
]
|
|
512
|
+
)
|
|
513
|
+
# 模拟 ensure_ai_img_plan 已先跑:p2 的 ai_p2.png 已在、且有一条客户图增强条目。
|
|
514
|
+
self._img_plan(
|
|
515
|
+
[
|
|
516
|
+
{"file": "enhanced_src_1.png", "mode": "image-to-image", "source_file": "src_1.png", "prompt": "x"},
|
|
517
|
+
{"file": "ai_p2.png", "mode": "text-to-image", "aspect": "16:9", "prompt": "a calm industrial backdrop, no text"},
|
|
518
|
+
]
|
|
519
|
+
)
|
|
520
|
+
rc = psl.ensure_page_backgrounds(self.d)
|
|
521
|
+
self.assertEqual(rc, 0)
|
|
522
|
+
ip = self._read_plan()
|
|
523
|
+
by_file = {e["file"]: e for e in ip}
|
|
524
|
+
# 无背景内容页 p3/p4/p5/p6 各补一条氛围底图(约定名)。
|
|
525
|
+
for pg in (3, 4, 5, 6):
|
|
526
|
+
self.assertIn(f"ai_p{pg}.png", by_file, f"p{pg} 应补氛围底图")
|
|
527
|
+
self.assertEqual(by_file[f"ai_p{pg}.png"]["mode"], "text-to-image")
|
|
528
|
+
self.assertEqual(by_file[f"ai_p{pg}.png"]["aspect"], "16:9")
|
|
529
|
+
# 已有背景的页不补:p1(客户图底)、p2(ai-* 带 prompt,已在)。
|
|
530
|
+
self.assertNotIn("ai_p1.png", by_file)
|
|
531
|
+
# p2 仍是原 ai_p2 那条、未被重复注入。
|
|
532
|
+
self.assertEqual(sum(1 for e in ip if e["file"] == "ai_p2.png"), 1)
|
|
533
|
+
# 客户图增强条目保留。
|
|
534
|
+
self.assertIn("enhanced_src_1.png", by_file)
|
|
535
|
+
# 总数 = 原 2 条 + 新 4 条 = 6。
|
|
536
|
+
self.assertEqual(len(ip), 6)
|
|
537
|
+
|
|
538
|
+
def test_source_enhanced_customer_visual_pages_never_get_independent_ai_bg(self):
|
|
539
|
+
# 2026-07-03 真机 3/5/6/8/9 页事故回归锁:route=source-enhanced 的页(无论 hero/contain/gallery/bg)
|
|
540
|
+
# 一律不再注入独立 ai_p<page>.png,`.edit-bg` 交给 build_batch 既有的 VI 渐变兜底。
|
|
541
|
+
self._design(
|
|
542
|
+
[
|
|
543
|
+
{"page": 3, "visual": {"route": "source-enhanced", "usage_intent": "source_enhanced_hero", "source_file": "src_2.jpg"}},
|
|
544
|
+
{"page": 5, "visual": {"route": "source-enhanced", "usage_intent": "direct_contain", "source_file": "src_6.png"}},
|
|
545
|
+
{"page": 6, "visual": {"route": "source-enhanced", "usage_intent": "gallery", "source_file": "src_3.png+src_4.png+src_5.png"}},
|
|
546
|
+
{"page": 9, "visual": {"route": "source-enhanced", "usage_intent": "source_enhanced_hero", "source_file": "src_9.png"}},
|
|
547
|
+
]
|
|
548
|
+
)
|
|
549
|
+
rc = psl.ensure_page_backgrounds(self.d)
|
|
550
|
+
self.assertEqual(rc, 0)
|
|
551
|
+
ip = self._read_plan()
|
|
552
|
+
by_file = {e["file"]: e for e in ip}
|
|
553
|
+
for pg in (3, 5, 6, 9):
|
|
554
|
+
self.assertNotIn(f"ai_p{pg}.png", by_file, f"p{pg} 有客户图视觉,不应再补独立 AI 背景")
|
|
555
|
+
self.assertEqual(ip, [])
|
|
556
|
+
|
|
557
|
+
def test_no_customer_image_page_still_gets_ai_bg_control_group(self):
|
|
558
|
+
# 对照组(页7 真机验证:无客户图页,独立 AI 背景 + VI 渐变兜底效果正常)——回归锁,确保
|
|
559
|
+
# 「路由收紧」没有误伤真正需要背景图的页。
|
|
560
|
+
self._design([{"page": 7, "visual": {"route": "html"}}])
|
|
561
|
+
psl.ensure_page_backgrounds(self.d)
|
|
562
|
+
by_file = {e["file"]: e for e in self._read_plan()}
|
|
563
|
+
self.assertIn("ai_p7.png", by_file)
|
|
564
|
+
|
|
565
|
+
def test_atmos_prompt_shape(self):
|
|
566
|
+
self._design([{"page": 3, "visual": {"route": "html"}}])
|
|
567
|
+
psl.ensure_page_backgrounds(self.d)
|
|
568
|
+
ent = next(e for e in self._read_plan() if e["file"] == "ai_p3.png")
|
|
569
|
+
# 氛围底图提示词:产品详情页氛围 + 含质量词 + 以 no-text 尾收口(不画结构图,免与前景打架)。
|
|
570
|
+
self.assertIn("atmospheric", ent["prompt"])
|
|
571
|
+
self.assertIn("no diagrams", ent["prompt"])
|
|
572
|
+
self.assertTrue(ent["prompt"].rstrip().endswith(psl._NO_TEXT_TAIL))
|
|
573
|
+
|
|
574
|
+
def test_semantic_page_prompt_takes_priority(self):
|
|
575
|
+
self._design([
|
|
576
|
+
{
|
|
577
|
+
"page": 8,
|
|
578
|
+
"one_sentence_takeaway": "安装过程从经验依赖变成可复核路径",
|
|
579
|
+
"visual_story": "用真实装配现场让观众先看到落地方法",
|
|
580
|
+
"main_visual_layer": "航空器总装车间场景",
|
|
581
|
+
"text_overlay_zone": "left",
|
|
582
|
+
"visual": {"route": "html"},
|
|
583
|
+
}
|
|
584
|
+
])
|
|
585
|
+
psl.ensure_page_backgrounds(self.d)
|
|
586
|
+
ent = next(e for e in self._read_plan() if e["file"] == "ai_p8.png")
|
|
587
|
+
self.assertIn("安装过程", ent["prompt"])
|
|
588
|
+
self.assertIn("航空器总装车间", ent["prompt"])
|
|
589
|
+
self.assertIn("no text", ent["prompt"].lower())
|
|
590
|
+
|
|
591
|
+
def test_i2i_style_semantic_prompt_on_non_source_page_becomes_complementary_background(self):
|
|
592
|
+
# route≠source-enhanced 的页(本函数仍会补背景)若 semantic_image_prompt 本身写成了 i2i 指令
|
|
593
|
+
# (design 误写/沿用了别处的 i2i 提示词),仍需转成不抢戏的互补背景,而不是原样喂给 t2i
|
|
594
|
+
# (模型没有参考图时会随机生成瓶子/设备等无关主体)。route=source-enhanced 的页现在由
|
|
595
|
+
# ensure_page_backgrounds 直接跳过整个背景注入,不会走到这条互补背景生成路径。
|
|
596
|
+
self._design([
|
|
597
|
+
{
|
|
598
|
+
"page": 9,
|
|
599
|
+
"one_sentence_takeaway": "多终端沉浸式调阅让档案进入现场",
|
|
600
|
+
"visual_story": "前景插图展示电脑、VR 眼镜和移动终端组合",
|
|
601
|
+
"semantic_image_prompt": (
|
|
602
|
+
"Use the attached image as the strict visual reference: "
|
|
603
|
+
"preserve its main subject, content and real colors exactly."
|
|
604
|
+
),
|
|
605
|
+
"text_overlay_zone": "left",
|
|
606
|
+
"visual": {"route": "ai-B"},
|
|
607
|
+
}
|
|
608
|
+
])
|
|
609
|
+
psl.ensure_page_backgrounds(self.d)
|
|
610
|
+
ent = next(e for e in self._read_plan() if e["file"] == "ai_p9.png")
|
|
611
|
+
prompt = ent["prompt"].lower()
|
|
612
|
+
self.assertIn("complementary", prompt)
|
|
613
|
+
self.assertIn("separate customer-image foreground", prompt)
|
|
614
|
+
self.assertNotIn("use the attached image", prompt)
|
|
615
|
+
self.assertNotIn("preserve its main subject", prompt)
|
|
616
|
+
self.assertIn("no bottle", prompt)
|
|
617
|
+
self.assertIn("no standalone product", prompt)
|
|
618
|
+
|
|
619
|
+
def test_per_page_variant_differs(self):
|
|
620
|
+
# 相邻补背景的页取不同光位变体(确定性、非随机),避免氛围底图完全雷同。
|
|
621
|
+
self._design([{"page": 3, "visual": {"route": "html"}}, {"page": 4, "visual": {"route": "none"}}])
|
|
622
|
+
psl.ensure_page_backgrounds(self.d)
|
|
623
|
+
by_file = {e["file"]: e for e in self._read_plan()}
|
|
624
|
+
self.assertNotEqual(by_file["ai_p3.png"]["prompt"], by_file["ai_p4.png"]["prompt"])
|
|
625
|
+
|
|
626
|
+
def test_dedups_existing_ai_file(self):
|
|
627
|
+
# img_plan 已有 ai_p3.png(如 ensure_ai_img_plan 注入)→ 不重复注入。
|
|
628
|
+
self._design([{"page": 3, "visual": {"route": "html"}}])
|
|
629
|
+
self._img_plan([{"file": "ai_p3.png", "mode": "text-to-image", "aspect": "16:9", "prompt": "preexisting, no text"}])
|
|
630
|
+
psl.ensure_page_backgrounds(self.d)
|
|
631
|
+
ip = self._read_plan()
|
|
632
|
+
self.assertEqual(sum(1 for e in ip if e["file"] == "ai_p3.png"), 1)
|
|
633
|
+
self.assertEqual(ip[0]["prompt"], "preexisting, no text") # 原条目不被覆盖
|
|
634
|
+
|
|
635
|
+
def test_idempotent(self):
|
|
636
|
+
self._design([{"page": 3, "visual": {"route": "html"}}, {"page": 4, "visual": {"route": "none"}}])
|
|
637
|
+
psl.ensure_page_backgrounds(self.d)
|
|
638
|
+
n1 = len(self._read_plan())
|
|
639
|
+
psl.ensure_page_backgrounds(self.d)
|
|
640
|
+
self.assertEqual(len(self._read_plan()), n1)
|
|
641
|
+
|
|
642
|
+
def test_no_design_no_crash(self):
|
|
643
|
+
rc = psl.ensure_page_backgrounds(self.d)
|
|
644
|
+
self.assertEqual(rc, 0)
|
|
645
|
+
|
|
646
|
+
def test_creates_img_plan_when_absent(self):
|
|
647
|
+
# img_plan 还不存在时也能创建并注入。
|
|
648
|
+
self._design([{"page": 1, "visual": {"route": "none"}}])
|
|
649
|
+
psl.ensure_page_backgrounds(self.d)
|
|
650
|
+
self.assertEqual([e["file"] for e in self._read_plan()], ["ai_p1.png"])
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class TestReconcileMissing(unittest.TestCase):
|
|
654
|
+
"""生图后确定性兜底:缺失的 enhanced 派生图从原图拷贝顶上(防 deck 死链空框)。"""
|
|
655
|
+
|
|
656
|
+
def _setup_build(self, images, assets_files):
|
|
657
|
+
d = tempfile.mkdtemp(prefix="srclayout-rec-")
|
|
658
|
+
assets = os.path.join(d, "assets")
|
|
659
|
+
os.makedirs(assets, exist_ok=True)
|
|
660
|
+
with open(os.path.join(d, "material.json"), "w", encoding="utf-8") as f:
|
|
661
|
+
json.dump({"has_source": True, "images": images}, f, ensure_ascii=False)
|
|
662
|
+
plan = psl.build_plan(d)
|
|
663
|
+
with open(os.path.join(d, "source_layout_plan.json"), "w", encoding="utf-8") as f:
|
|
664
|
+
json.dump(plan, f, ensure_ascii=False)
|
|
665
|
+
for name, content in assets_files.items():
|
|
666
|
+
with open(os.path.join(assets, name), "wb") as f:
|
|
667
|
+
f.write(content)
|
|
668
|
+
return d, assets, plan
|
|
669
|
+
|
|
670
|
+
def test_missing_enhanced_copied_from_source(self):
|
|
671
|
+
# 低清 bg 图 → source_enhanced_bg、enhance=true,enhanced_src_1.jpg ← src_1.jpg
|
|
672
|
+
d, assets, plan = self._setup_build(
|
|
673
|
+
[_img("src_1.jpg", 800, 450, vision=_vision("山景", bg=True))],
|
|
674
|
+
{"src_1.jpg": b"ORIGINAL-IMAGE-BYTES"}, # 原图在、增强图缺
|
|
675
|
+
)
|
|
676
|
+
self.assertEqual(plan["enhance_entries"][0]["file"], "enhanced_src_1.jpg")
|
|
677
|
+
self.assertEqual(plan["enhance_entries"][0]["source_file"], "src_1.jpg")
|
|
678
|
+
self.assertEqual(psl.reconcile_missing(d), 0)
|
|
679
|
+
enhanced = os.path.join(assets, "enhanced_src_1.jpg")
|
|
680
|
+
self.assertTrue(os.path.isfile(enhanced)) # 兜底拷出来了
|
|
681
|
+
with open(enhanced, "rb") as f:
|
|
682
|
+
self.assertEqual(f.read(), b"ORIGINAL-IMAGE-BYTES") # 字节 = 原图
|
|
683
|
+
|
|
684
|
+
def test_existing_enhanced_not_overwritten(self):
|
|
685
|
+
d, assets, _ = self._setup_build(
|
|
686
|
+
[_img("src_1.jpg", 800, 450, vision=_vision("山景", bg=True))],
|
|
687
|
+
{"src_1.jpg": b"ORIGINAL", "enhanced_src_1.jpg": b"REAL-ENHANCED"}, # 真增强图已在
|
|
688
|
+
)
|
|
689
|
+
psl.reconcile_missing(d)
|
|
690
|
+
with open(os.path.join(assets, "enhanced_src_1.jpg"), "rb") as f:
|
|
691
|
+
self.assertEqual(f.read(), b"REAL-ENHANCED") # 绝不被原图覆盖
|
|
692
|
+
|
|
693
|
+
def test_idempotent(self):
|
|
694
|
+
d, assets, _ = self._setup_build(
|
|
695
|
+
[_img("src_1.jpg", 800, 450, vision=_vision("山景", bg=True))],
|
|
696
|
+
{"src_1.jpg": b"ORIG"},
|
|
697
|
+
)
|
|
698
|
+
self.assertEqual(psl.reconcile_missing(d), 0)
|
|
699
|
+
self.assertEqual(psl.reconcile_missing(d), 0) # 再跑不报错、幂等
|
|
700
|
+
with open(os.path.join(assets, "enhanced_src_1.jpg"), "rb") as f:
|
|
701
|
+
self.assertEqual(f.read(), b"ORIG")
|
|
702
|
+
|
|
703
|
+
def test_missing_source_no_crash(self):
|
|
704
|
+
d, assets, _ = self._setup_build(
|
|
705
|
+
[_img("src_1.jpg", 800, 450, vision=_vision("山景", bg=True))],
|
|
706
|
+
{}, # 连原图都没有
|
|
707
|
+
)
|
|
708
|
+
self.assertEqual(psl.reconcile_missing(d), 0) # 恒成功、不崩
|
|
709
|
+
self.assertFalse(os.path.isfile(os.path.join(assets, "enhanced_src_1.jpg")))
|
|
710
|
+
|
|
711
|
+
def test_no_plan_no_crash(self):
|
|
712
|
+
d = tempfile.mkdtemp(prefix="srclayout-rec-empty-")
|
|
713
|
+
self.assertEqual(psl.reconcile_missing(d), 0) # 无 source_layout_plan.json 也恒 0
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
class TestEnhanceStyle(unittest.TestCase):
|
|
717
|
+
"""图生图三态:bg(背景换干净底) / hero(场景换干净底) / cutout(离散主体抠透明前景插图)。"""
|
|
718
|
+
|
|
719
|
+
def test_discrete_subject_hero_is_cutout_transparent(self):
|
|
720
|
+
# 低清 + 离散主体(产品/设备/相机)+ 非背景 → source_enhanced_hero + cutout 透明
|
|
721
|
+
e = psl.plan_one(_img("p.png", 600, 900, vision=_vision("一台手持工业相机的产品特写", bg=False)))
|
|
722
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
723
|
+
self.assertEqual(e["enhance_style"], "cutout")
|
|
724
|
+
self.assertIn("transparent", e["enhance_prompt"].lower()) # 抠透明背景
|
|
725
|
+
|
|
726
|
+
def test_multi_terminal_devices_are_cutout_not_dense(self):
|
|
727
|
+
e = psl.plan_one(
|
|
728
|
+
_img(
|
|
729
|
+
"terminals.png",
|
|
730
|
+
1400,
|
|
731
|
+
900,
|
|
732
|
+
vision=_vision(
|
|
733
|
+
"多种播放终端产品展示,包括电脑显示器、VR眼镜、平板和手机,纯色渐变背景",
|
|
734
|
+
bg=False,
|
|
735
|
+
elements=["播放终端", "电脑", "VR眼镜", "手机"],
|
|
736
|
+
),
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
self.assertFalse(e["is_dense_info"])
|
|
740
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
741
|
+
self.assertEqual(e["enhance_style"], "cutout")
|
|
742
|
+
self.assertEqual(e["image_role"], "foreground_cutout")
|
|
743
|
+
self.assertEqual(e["target_layer"], "foreground")
|
|
744
|
+
self.assertIn("transparent", e["enhance_prompt"].lower())
|
|
745
|
+
|
|
746
|
+
def test_terminal_interface_screenshot_remains_dense(self):
|
|
747
|
+
e = psl.plan_one(
|
|
748
|
+
_img(
|
|
749
|
+
"terminal_ui.png",
|
|
750
|
+
1200,
|
|
751
|
+
800,
|
|
752
|
+
vision=_vision(
|
|
753
|
+
"终端界面截图,包含表格、文字、数据列表和控制台状态",
|
|
754
|
+
bg=False,
|
|
755
|
+
elements=["终端界面", "表格", "数据列表"],
|
|
756
|
+
),
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
self.assertTrue(e["is_dense_info"])
|
|
760
|
+
self.assertEqual(e["enhance_style"], "dense")
|
|
761
|
+
self.assertEqual(e["image_role"], "faithful_document")
|
|
762
|
+
|
|
763
|
+
def test_scene_hero_is_clean_bg_not_transparent(self):
|
|
764
|
+
# 低清 + 场景(车间/现场)+ 非背景 → source_enhanced_hero + hero(换干净底),非透明
|
|
765
|
+
e = psl.plan_one(_img("s.png", 600, 900, vision=_vision("航空器总装车间现场", bg=False)))
|
|
766
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
767
|
+
self.assertEqual(e["enhance_style"], "hero")
|
|
768
|
+
self.assertNotIn("transparent", e["enhance_prompt"].lower())
|
|
769
|
+
self.assertIn("backdrop", e["enhance_prompt"].lower())
|
|
770
|
+
|
|
771
|
+
def test_bg_style_unchanged(self):
|
|
772
|
+
e = psl.plan_one(_img("b.png", 800, 450, vision=_vision("开阔山景风光", bg=True)))
|
|
773
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
774
|
+
self.assertEqual(e["enhance_style"], "bg")
|
|
775
|
+
self.assertIn("background", e["enhance_prompt"].lower())
|
|
776
|
+
self.assertNotIn("transparent", e["enhance_prompt"].lower())
|
|
777
|
+
|
|
778
|
+
def test_scene_and_subject_keyword_scene_wins(self):
|
|
779
|
+
# 同时含场景词+主体词 → 场景优先、不抠透明(保守不误抠掉环境)
|
|
780
|
+
e = psl.plan_one(_img("m.png", 600, 900, vision=_vision("车间里的一台设备", bg=False)))
|
|
781
|
+
self.assertEqual(e["enhance_style"], "hero")
|
|
782
|
+
|
|
783
|
+
def test_no_vision_has_no_style(self):
|
|
784
|
+
# 全图生图后,唯一 enhance=false 路径 = 无 vision 盲态 → enhance_style None。
|
|
785
|
+
e = psl.plan_one(_img("d.png", 1920, 1080)) # 无 vision
|
|
786
|
+
self.assertFalse(e["enhance_required"])
|
|
787
|
+
self.assertIsNone(e["enhance_style"])
|
|
788
|
+
|
|
789
|
+
def test_real_scene_hero_not_cutout(self):
|
|
790
|
+
# ③ real_scene 非背景图 → hero(保场景、不去背景),绝非 cutout 抠透明。
|
|
791
|
+
e = psl.plan_one(_img("rs.png", 1600, 1200, vision=_vision("智能工厂车间生产线现场", bg=False)))
|
|
792
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
793
|
+
self.assertEqual(e["background_kind"], "real_scene")
|
|
794
|
+
self.assertEqual(e["enhance_style"], "hero")
|
|
795
|
+
self.assertNotIn("transparent", e["enhance_prompt"].lower())
|
|
796
|
+
|
|
797
|
+
def test_flat_base_discrete_subject_cutout(self):
|
|
798
|
+
# ③ flat_base(白底/棚拍)+ 离散主体(产品/相机)→ cutout 抠透明前景插图。
|
|
799
|
+
e = psl.plan_one(_img("fb.png", 600, 900, vision=_vision("白底棚拍的一台工业相机产品", bg=False)))
|
|
800
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
801
|
+
self.assertEqual(e["background_kind"], "flat_base")
|
|
802
|
+
self.assertEqual(e["enhance_style"], "cutout")
|
|
803
|
+
self.assertIn("transparent", e["enhance_prompt"].lower())
|
|
804
|
+
|
|
805
|
+
def test_three_states_have_quality_words(self):
|
|
806
|
+
# ④ bg/hero/cutout 三态提示词都含高清质量词;bg 另含 cinematic depth、cutout 另含 crisp cut-out edges。
|
|
807
|
+
bg = psl.plan_one(_img("b.png", 800, 450, vision=_vision("开阔山景风光", bg=True)))
|
|
808
|
+
hero = psl.plan_one(_img("h.png", 600, 900, vision=_vision("航空器总装车间现场", bg=False)))
|
|
809
|
+
cutout = psl.plan_one(_img("c.png", 600, 900, vision=_vision("白底棚拍的一台工业相机产品", bg=False)))
|
|
810
|
+
self.assertEqual(bg["enhance_style"], "bg")
|
|
811
|
+
self.assertEqual(hero["enhance_style"], "hero")
|
|
812
|
+
self.assertEqual(cutout["enhance_style"], "cutout")
|
|
813
|
+
for e in (bg, hero, cutout):
|
|
814
|
+
p = e["enhance_prompt"].lower()
|
|
815
|
+
self.assertIn("high resolution", p)
|
|
816
|
+
self.assertIn("sharp", p)
|
|
817
|
+
self.assertIn("crisp cut-out edges", cutout["enhance_prompt"].lower())
|
|
818
|
+
self.assertIn("cinematic depth", bg["enhance_prompt"].lower())
|
|
819
|
+
|
|
820
|
+
# ---- 用户细化(2026-06-27):带文字图不能直接当背景 + flat 底前景图抠透明(两层底打架)----
|
|
821
|
+
|
|
822
|
+
def test_flat_base_nondiscrete_foreground_is_cutout(self):
|
|
823
|
+
# flat 底(渐变/纹理/白底)的【非离散主体】前景图 → 也抠透明(治「带自有底纹的图放我们背景上=两层底打架」)。
|
|
824
|
+
e = psl.plan_one(_img("fg.png", 600, 900, vision=_vision("渐变色纹理底上的一个抽象品牌图形", bg=False)))
|
|
825
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero")
|
|
826
|
+
self.assertEqual(e["background_kind"], "flat_base")
|
|
827
|
+
self.assertEqual(e["enhance_style"], "cutout")
|
|
828
|
+
self.assertIn("transparent", e["enhance_prompt"].lower())
|
|
829
|
+
self.assertIn("no texture", e["enhance_prompt"].lower()) # 连背景纹理一起抠掉
|
|
830
|
+
|
|
831
|
+
def test_bg_with_text_forces_textfree_redraw(self):
|
|
832
|
+
# 背景图自带文字/横幅/水印 → bg 态换「允许重绘 + 强制抹字」前导(治「带字图当背景露字」)。
|
|
833
|
+
e = psl.plan_one(_img("bt.png", 1280, 720, vision=_vision("印有标语横幅文字的开阔风光大图", bg=True)))
|
|
834
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
835
|
+
self.assertEqual(e["enhance_style"], "bg")
|
|
836
|
+
self.assertTrue(e["has_embedded_text"])
|
|
837
|
+
p = e["enhance_prompt"]
|
|
838
|
+
self.assertIn("REMOVE", p) # 强制抹掉所有文字
|
|
839
|
+
self.assertIn("text-free", p.lower())
|
|
840
|
+
self.assertNotIn("do not replace, restyle or invent", p) # 不再用严格保内容前导
|
|
841
|
+
|
|
842
|
+
def test_bg_without_text_keeps_strict_preserve(self):
|
|
843
|
+
# 背景图无文字 → 仍用严格「保内容不换主体」前导(向后兼容)。
|
|
844
|
+
e = psl.plan_one(_img("b.png", 1280, 720, vision=_vision("开阔山景风光", bg=True)))
|
|
845
|
+
self.assertEqual(e["enhance_style"], "bg")
|
|
846
|
+
self.assertFalse(e["has_embedded_text"])
|
|
847
|
+
self.assertIn("do not replace, restyle or invent", e["enhance_prompt"])
|
|
848
|
+
|
|
849
|
+
def test_hero_with_text_removes_text(self):
|
|
850
|
+
# 场景 hero 图带水印 → hero 态也强制抹字。
|
|
851
|
+
e = psl.plan_one(_img("h.png", 1600, 1200, vision=_vision("带水印的智能工厂车间生产线现场", bg=False)))
|
|
852
|
+
self.assertEqual(e["enhance_style"], "hero")
|
|
853
|
+
self.assertTrue(e["has_embedded_text"])
|
|
854
|
+
self.assertIn("REMOVE", e["enhance_prompt"])
|
|
855
|
+
|
|
856
|
+
def test_has_embedded_text_detection(self):
|
|
857
|
+
self.assertTrue(psl._has_embedded_text(_vision("一张带标题文字的封面图")))
|
|
858
|
+
self.assertTrue(psl._has_embedded_text(_vision("product shot with a watermark")))
|
|
859
|
+
self.assertFalse(psl._has_embedded_text(_vision("开阔山景风光")))
|
|
860
|
+
self.assertFalse(psl._has_embedded_text(None))
|
|
861
|
+
|
|
862
|
+
def test_discrete_subject_never_full_background(self):
|
|
863
|
+
# 用户红线(2026-06-27 推荐改动):离散主体(产品/设备)即便被识图标 suitable_as_background,
|
|
864
|
+
# 也【不当整页背景】(主体+自有底纹放我们背景上=两层底打架) → 转前景主视觉 + cutout 透明。
|
|
865
|
+
e = psl.plan_one(_img("p.png", 1920, 1080, vision=_vision("白底棚拍的一台工业相机产品", bg=True)))
|
|
866
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_hero") # 非 source_enhanced_bg
|
|
867
|
+
self.assertEqual(e["enhance_style"], "cutout")
|
|
868
|
+
self.assertIn("transparent", e["enhance_prompt"].lower())
|
|
869
|
+
|
|
870
|
+
def test_scene_background_still_full_background(self):
|
|
871
|
+
# 反证:真·场景背景图(非离散主体)仍走整页底图,不受上面红线影响。
|
|
872
|
+
e = psl.plan_one(_img("s.png", 1920, 1080, vision=_vision("辽阔的数据中心机房全景", bg=True)))
|
|
873
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
874
|
+
self.assertEqual(e["enhance_style"], "bg")
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
class TestEmbeddedTextExtraction(unittest.TestCase):
|
|
878
|
+
"""2026-07-03:src_6 真机事故(enhanced_src_6.png 原样保留了「AI全景相机优势」标题+标签)——
|
|
879
|
+
识图结构化 `embedded_text` 字段优先于关键词启发式;抹字指令引用实际抄下来的文字;dense 且
|
|
880
|
+
content_type=screenshot_slide/poster 时新增「去装饰性 chrome 文字、保结构」分支;chart 类型
|
|
881
|
+
数据标签不受影响(回归锁)。"""
|
|
882
|
+
|
|
883
|
+
def test_structured_field_takes_priority_over_keyword_heuristic(self):
|
|
884
|
+
# 文案里没有任何"文字/水印"类关键词,但 embedded_text 非空 → 仍判 True(结构化数据优先)。
|
|
885
|
+
v = _vision("一张开阔风光照", embedded_text=[{"text": "夏日促销", "region": "右下角"}])
|
|
886
|
+
self.assertTrue(psl._has_embedded_text(v))
|
|
887
|
+
|
|
888
|
+
def test_structured_field_explicit_empty_overrides_keyword_false_positive(self):
|
|
889
|
+
# 字段存在且为空列表 = 识图明确判过"没有",即便 description 里出现"标题"字样也不再猜。
|
|
890
|
+
v = {"description": "一张封面标题风格的摄影作品", "embedded_text": []}
|
|
891
|
+
self.assertFalse(psl._has_embedded_text(v))
|
|
892
|
+
|
|
893
|
+
def test_missing_field_falls_back_to_keyword_heuristic(self):
|
|
894
|
+
# 旧格式识图 JSON(无 embedded_text 键)→ 退回关键词启发式,向后兼容。
|
|
895
|
+
self.assertTrue(psl._has_embedded_text(_vision("一张带标题文字的封面图")))
|
|
896
|
+
self.assertFalse(psl._has_embedded_text(_vision("开阔山景风光")))
|
|
897
|
+
|
|
898
|
+
def test_bg_prompt_quotes_actual_embedded_text(self):
|
|
899
|
+
# bg 分支:有 embedded_text 时抹字指令引用真实抄下来的文字,而非泛泛措辞。
|
|
900
|
+
v = _vision("印有标语横幅文字的开阔风光大图", bg=True,
|
|
901
|
+
embedded_text=[{"text": "限时特惠", "region": "顶部横幅"}])
|
|
902
|
+
e = psl.plan_one(_img("bt.png", 1280, 720, vision=v))
|
|
903
|
+
p = e["enhance_prompt"]
|
|
904
|
+
self.assertIn("限时特惠", p)
|
|
905
|
+
self.assertIn("顶部横幅", p)
|
|
906
|
+
self.assertIn("REMOVED", p)
|
|
907
|
+
self.assertNotIn("has text, captions, logos or watermarks on it", p) # 不再是泛化措辞
|
|
908
|
+
|
|
909
|
+
def test_bg_prompt_falls_back_to_generic_wording_without_embedded_text(self):
|
|
910
|
+
# 旧格式(has_text=True 但无 embedded_text,纯关键词猜)→ 保持原泛化措辞,不报错。
|
|
911
|
+
v = _vision("印有标语横幅文字的开阔风光大图", bg=True)
|
|
912
|
+
e = psl.plan_one(_img("bt2.png", 1280, 720, vision=v))
|
|
913
|
+
self.assertIn("has text, captions, logos or watermarks on it", e["enhance_prompt"])
|
|
914
|
+
|
|
915
|
+
def test_dense_screenshot_slide_with_embedded_text_strips_caption_preserves_structure(self):
|
|
916
|
+
# src_6 真机形态:content_type=screenshot_slide + embedded_text → 保结构去装饰字。
|
|
917
|
+
v = _vision(
|
|
918
|
+
"一张完整的演示页截图,标题「AI全景相机优势」,含对比排版",
|
|
919
|
+
content_type="screenshot_slide",
|
|
920
|
+
embedded_text=[
|
|
921
|
+
{"text": "AI全景相机优势", "region": "顶部标题"},
|
|
922
|
+
{"text": "多台监控", "region": "左侧标签"},
|
|
923
|
+
{"text": "1台全景", "region": "左侧标签"},
|
|
924
|
+
],
|
|
925
|
+
)
|
|
926
|
+
e = psl.plan_one(_img("src_6.png", 1432, 792, vision=v))
|
|
927
|
+
self.assertEqual(e["usage_intent"], "direct_contain")
|
|
928
|
+
self.assertEqual(e["enhance_style"], "dense")
|
|
929
|
+
self.assertEqual(e["text_policy"], "remove_all_text") # 复用既有枚举值,不新造
|
|
930
|
+
p = e["enhance_prompt"]
|
|
931
|
+
self.assertIn("AI全景相机优势", p)
|
|
932
|
+
self.assertIn("多台监控", p)
|
|
933
|
+
self.assertIn("1台全景", p)
|
|
934
|
+
self.assertIn("REMOVED", p)
|
|
935
|
+
self.assertIn("layout, sub-images, comparison structure", p) # 结构仍要求保留
|
|
936
|
+
self.assertNotIn("every data point, label, axis, value and structure unchanged", p) # 旧全保真措辞不出现(那是"绝不去字"的措辞)
|
|
937
|
+
self.assertEqual(e["embedded_text"][0]["text"], "AI全景相机优势")
|
|
938
|
+
|
|
939
|
+
def test_dense_chart_type_unaffected_regression(self):
|
|
940
|
+
# 回归锁:真正的图表(数据标签=数据本身)即便识图误标了 embedded_text,也不能被去字——
|
|
941
|
+
# 只有 screenshot_slide/poster 才走去字分支,chart 维持原有纯保真行为。
|
|
942
|
+
v = _vision(
|
|
943
|
+
"巡检效率对比柱状图", content_type="chart",
|
|
944
|
+
embedded_text=[{"text": "68%", "region": "柱顶标签"}],
|
|
945
|
+
)
|
|
946
|
+
e = psl.plan_one(_img("chart.png", 1200, 800, vision=v))
|
|
947
|
+
self.assertEqual(e["enhance_style"], "dense")
|
|
948
|
+
self.assertEqual(e["text_policy"], "preserve_data_text") # 不受影响
|
|
949
|
+
p = e["enhance_prompt"]
|
|
950
|
+
self.assertIn("every data point, label, axis, value and structure unchanged", p)
|
|
951
|
+
self.assertNotIn("REMOVED", p)
|
|
952
|
+
|
|
953
|
+
def test_dense_screenshot_slide_without_embedded_text_unaffected(self):
|
|
954
|
+
# screenshot_slide 但识图没抄到任何文字(embedded_text=[])→ 仍走原保真分支,不强行造词去字。
|
|
955
|
+
v = _vision("一张干净的产品展示截图", content_type="screenshot_slide", embedded_text=[])
|
|
956
|
+
e = psl.plan_one(_img("clean.png", 1600, 900, vision=v))
|
|
957
|
+
self.assertEqual(e["text_policy"], "preserve_data_text")
|
|
958
|
+
self.assertNotIn("embedded_text", e)
|
|
959
|
+
|
|
960
|
+
def test_embedded_text_absent_from_entry_when_empty(self):
|
|
961
|
+
e = psl.plan_one(_img("plain.png", 1500, 1000, vision=_vision("普通产品照")))
|
|
962
|
+
self.assertNotIn("embedded_text", e)
|
|
963
|
+
|
|
964
|
+
def test_embedded_text_string_items_normalized(self):
|
|
965
|
+
# 兼容识图偶尔直接输出纯字符串数组(非 {text,region} 对象)而非严格 schema。
|
|
966
|
+
v = _vision("测试图", embedded_text=["纯字符串文字"])
|
|
967
|
+
self.assertEqual(psl._embedded_text_list(v), [{"text": "纯字符串文字", "region": ""}])
|
|
968
|
+
|
|
969
|
+
def test_plan_md_renders_recreate_prompt(self):
|
|
970
|
+
# 设计阶段只读 source_layout_plan.md——渲染是让它"看得见"去字信息的唯一途径。
|
|
971
|
+
vision = _vision(
|
|
972
|
+
"一张完整的演示页截图,标题「AI全景相机优势」", content_type="screenshot_slide",
|
|
973
|
+
embedded_text=[{"text": "AI全景相机优势", "region": "顶部标题"},
|
|
974
|
+
{"text": "多台监控", "region": "左箭头标签"}],
|
|
975
|
+
)
|
|
976
|
+
d = tempfile.mkdtemp(prefix="srclayout-et-")
|
|
977
|
+
with open(os.path.join(d, "material.json"), "w", encoding="utf-8") as f:
|
|
978
|
+
json.dump({"has_source": True, "images": [_img("src_6.png", 1432, 792, vision=vision)]},
|
|
979
|
+
f, ensure_ascii=False)
|
|
980
|
+
plan = psl.build_plan(d)
|
|
981
|
+
psl.write_plan_md(d, plan)
|
|
982
|
+
with open(os.path.join(d, "source_layout_plan.md"), encoding="utf-8") as f:
|
|
983
|
+
md = f.read()
|
|
984
|
+
self.assertIn("AI全景相机优势", md)
|
|
985
|
+
self.assertIn("多台监控", md)
|
|
986
|
+
self.assertIn("原图文字已在图生图阶段去除", md)
|
|
987
|
+
self.assertIn("用真实 HTML 文字重新配出", md)
|
|
988
|
+
|
|
989
|
+
def test_plan_md_no_recreate_line_when_no_embedded_text(self):
|
|
990
|
+
d = tempfile.mkdtemp(prefix="srclayout-et2-")
|
|
991
|
+
with open(os.path.join(d, "material.json"), "w", encoding="utf-8") as f:
|
|
992
|
+
json.dump({"has_source": True, "images": [_img("plain.png", 1500, 1000, vision=_vision("普通照片"))]},
|
|
993
|
+
f, ensure_ascii=False)
|
|
994
|
+
plan = psl.build_plan(d)
|
|
995
|
+
psl.write_plan_md(d, plan)
|
|
996
|
+
with open(os.path.join(d, "source_layout_plan.md"), encoding="utf-8") as f:
|
|
997
|
+
md = f.read()
|
|
998
|
+
self.assertNotIn("原图文字已在图生图阶段去除", md)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
class TestBackgroundKind(unittest.TestCase):
|
|
1002
|
+
"""② 识图驱动背景类型分流 _background_kind:real_scene / flat_base / unknown。"""
|
|
1003
|
+
|
|
1004
|
+
def test_real_scene(self):
|
|
1005
|
+
self.assertEqual(psl._background_kind(_vision("智能工厂车间生产线的现场实拍")), "real_scene")
|
|
1006
|
+
|
|
1007
|
+
def test_flat_base(self):
|
|
1008
|
+
self.assertEqual(psl._background_kind(_vision("产品在纯色渐变背景上的棚拍特写")), "flat_base")
|
|
1009
|
+
|
|
1010
|
+
def test_unknown(self):
|
|
1011
|
+
self.assertEqual(psl._background_kind(_vision("一杯热气腾腾的咖啡")), "unknown")
|
|
1012
|
+
|
|
1013
|
+
def test_real_scene_wins_over_flat_base(self):
|
|
1014
|
+
# 同含场景词+棚拍词 → real_scene 优先(保守保场景,别误抠/误换底)。
|
|
1015
|
+
self.assertEqual(psl._background_kind(_vision("车间现场的纯色背景棚拍")), "real_scene")
|
|
1016
|
+
|
|
1017
|
+
def test_palette_and_elements_counted(self):
|
|
1018
|
+
# description + palette_impression + key_elements 一起判(不止看 description)。
|
|
1019
|
+
v = {"description": "一台设备", "key_elements": ["白底"], "palette_impression": "纯色"}
|
|
1020
|
+
self.assertEqual(psl._background_kind(v), "flat_base")
|
|
1021
|
+
|
|
1022
|
+
def test_no_vision_unknown(self):
|
|
1023
|
+
self.assertEqual(psl._background_kind(None), "unknown")
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
class TestStructuredVisionFields(unittest.TestCase):
|
|
1027
|
+
"""识图结构化字段(2026-07-02 坠机图/旧 PPT 截图事故):relevant_to_topic / content_type。"""
|
|
1028
|
+
|
|
1029
|
+
def test_irrelevant_image_is_discarded(self):
|
|
1030
|
+
# relevant_to_topic=false(坠机资讯图与主题负相关)→ 弃用:不增强、不进模板、零槽位。
|
|
1031
|
+
v = _vision("坠机短视频封面,含黄色大字幕", bg=False)
|
|
1032
|
+
v["relevant_to_topic"] = False
|
|
1033
|
+
e = psl.plan_one(_img("src_1.jpg", 640, 480, vision=v))
|
|
1034
|
+
self.assertEqual(e["usage_intent"], "discard")
|
|
1035
|
+
self.assertFalse(e["enhance_required"])
|
|
1036
|
+
self.assertEqual(e["display_role"], "discard")
|
|
1037
|
+
self.assertEqual(e["min_canvas_fraction"], 0.0)
|
|
1038
|
+
self.assertEqual(e["layout_constraint"], "do_not_use")
|
|
1039
|
+
self.assertEqual(e["target_layer"], "none")
|
|
1040
|
+
self.assertEqual(e["recommended_templates"], [])
|
|
1041
|
+
self.assertNotIn("enhance_prompt", e)
|
|
1042
|
+
|
|
1043
|
+
def test_relevant_true_keeps_normal_flow(self):
|
|
1044
|
+
v = _vision("开阔的山景风光照", bg=True)
|
|
1045
|
+
v["relevant_to_topic"] = True
|
|
1046
|
+
e = psl.plan_one(_img("src_2.png", 1920, 1080, vision=v))
|
|
1047
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
1048
|
+
|
|
1049
|
+
def test_old_vision_without_fields_unchanged(self):
|
|
1050
|
+
# 旧 vision JSON(无新字段)→ 走既有分支,行为不变。
|
|
1051
|
+
e = psl.plan_one(_img("src_3.png", 1920, 1080, vision=_vision("开阔的山景风光照", bg=True)))
|
|
1052
|
+
self.assertEqual(e["usage_intent"], "source_enhanced_bg")
|
|
1053
|
+
|
|
1054
|
+
def test_screenshot_slide_forced_contain(self):
|
|
1055
|
+
# content_type=screenshot_slide(整版 PPT 截图,slide22 事故):即便描述词没命中 dense 启发,
|
|
1056
|
+
# 也强制 direct_contain + 忠实保真——绝不背景化。
|
|
1057
|
+
v = _vision("对比展示页面,上下两行内容", bg=True) # 描述刻意不含「截图/图表」关键词
|
|
1058
|
+
v["content_type"] = "screenshot_slide"
|
|
1059
|
+
e = psl.plan_one(_img("src_6.png", 1432, 792, vision=v))
|
|
1060
|
+
self.assertTrue(e["is_dense_info"])
|
|
1061
|
+
self.assertEqual(e["usage_intent"], "direct_contain")
|
|
1062
|
+
self.assertEqual(e["enhance_style"], "dense")
|
|
1063
|
+
self.assertEqual(e["target_layer"], "evidence")
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
class TestReconcileSourceExtensions(unittest.TestCase):
|
|
1067
|
+
"""design.json/img_plan.json 的客户图扩展名对齐 source_assets.json 真实文件名(治「design LLM
|
|
1068
|
+
猜错扩展名致生图找不到源图、静默退化占位图」——2026-07-05 真机 src_9 事故:design 猜 src_9.jpg,
|
|
1069
|
+
真实文件是 src_9.png,image-to-image 找不到源文件直接 degraded=gradient-png,deck 用了一张低质
|
|
1070
|
+
量渐变图当「安全与适航」页背景,而真正生成好的 enhanced_src_9.png 被晾在一边没被引用)。"""
|
|
1071
|
+
|
|
1072
|
+
def _setup(self, source_assets_files, design=None, img_plan=None):
|
|
1073
|
+
d = tempfile.mkdtemp(prefix="srclayout-reconcile-ext-")
|
|
1074
|
+
with open(os.path.join(d, "source_assets.json"), "w", encoding="utf-8") as f:
|
|
1075
|
+
json.dump({"images": [{"file": fn, "w": 800, "h": 600} for fn in source_assets_files]}, f)
|
|
1076
|
+
design_path = os.path.join(d, "design.json")
|
|
1077
|
+
if design is not None:
|
|
1078
|
+
with open(design_path, "w", encoding="utf-8") as f:
|
|
1079
|
+
json.dump(design, f, ensure_ascii=False)
|
|
1080
|
+
if img_plan is not None:
|
|
1081
|
+
with open(os.path.join(d, "img_plan.json"), "w", encoding="utf-8") as f:
|
|
1082
|
+
json.dump(img_plan, f, ensure_ascii=False)
|
|
1083
|
+
return d, design_path
|
|
1084
|
+
|
|
1085
|
+
def test_wrong_extension_corrected_in_img_plan_and_design(self):
|
|
1086
|
+
# 真机 src_9 事故复现:真实文件 src_9.png,design/img_plan 都猜成 .jpg。
|
|
1087
|
+
design = {"pages": [{"page": 10, "visual": {
|
|
1088
|
+
"route": "source-enhanced", "source_file": "src_9.jpg",
|
|
1089
|
+
"enhanced_file": "enhanced_src_9.jpg", "usage_intent": "source_enhanced_bg",
|
|
1090
|
+
}}]}
|
|
1091
|
+
img_plan = [{"file": "enhanced_src_9.jpg", "source_file": "src_9.jpg", "mode": "image-to-image"}]
|
|
1092
|
+
d, design_path = self._setup(["src_9.png"], design=design, img_plan=img_plan)
|
|
1093
|
+
rc = psl.reconcile_source_extensions(d, design_path)
|
|
1094
|
+
self.assertEqual(rc, 0)
|
|
1095
|
+
with open(design_path, encoding="utf-8") as f:
|
|
1096
|
+
fixed_design = json.load(f)
|
|
1097
|
+
visual = fixed_design["pages"][0]["visual"]
|
|
1098
|
+
self.assertEqual(visual["source_file"], "src_9.png")
|
|
1099
|
+
self.assertEqual(visual["enhanced_file"], "enhanced_src_9.png")
|
|
1100
|
+
with open(os.path.join(d, "img_plan.json"), encoding="utf-8") as f:
|
|
1101
|
+
fixed_plan = json.load(f)
|
|
1102
|
+
self.assertEqual(fixed_plan[0]["file"], "enhanced_src_9.png")
|
|
1103
|
+
self.assertEqual(fixed_plan[0]["source_file"], "src_9.png")
|
|
1104
|
+
|
|
1105
|
+
def test_dedup_when_corrected_entry_collides_with_existing_correct_one(self):
|
|
1106
|
+
# 纠正扩展名后与 source_layout_plan 已算好的正确条目撞车 → 去重保留一条,不留重复生图任务。
|
|
1107
|
+
img_plan = [
|
|
1108
|
+
{"file": "enhanced_src_9.jpg", "source_file": "src_9.jpg", "mode": "image-to-image", "usage_intent": "source_enhanced_bg"},
|
|
1109
|
+
{"file": "enhanced_src_9.png", "source_file": "src_9.png", "mode": "image-to-image", "usage_intent": "source_enhanced_hero"},
|
|
1110
|
+
]
|
|
1111
|
+
d, design_path = self._setup(["src_9.png"], design={"pages": []}, img_plan=img_plan)
|
|
1112
|
+
psl.reconcile_source_extensions(d, design_path)
|
|
1113
|
+
with open(os.path.join(d, "img_plan.json"), encoding="utf-8") as f:
|
|
1114
|
+
fixed_plan = json.load(f)
|
|
1115
|
+
self.assertEqual(len(fixed_plan), 1, fixed_plan)
|
|
1116
|
+
self.assertEqual(fixed_plan[0]["file"], "enhanced_src_9.png")
|
|
1117
|
+
|
|
1118
|
+
def test_no_mismatch_is_noop(self):
|
|
1119
|
+
# 扩展名本来就对 → 不改动,幂等。
|
|
1120
|
+
design = {"pages": [{"page": 1, "visual": {"source_file": "src_1.jpg", "enhanced_file": "enhanced_src_1.jpg"}}]}
|
|
1121
|
+
img_plan = [{"file": "enhanced_src_1.jpg", "source_file": "src_1.jpg"}]
|
|
1122
|
+
d, design_path = self._setup(["src_1.jpg"], design=design, img_plan=img_plan)
|
|
1123
|
+
rc = psl.reconcile_source_extensions(d, design_path)
|
|
1124
|
+
self.assertEqual(rc, 0)
|
|
1125
|
+
with open(design_path, encoding="utf-8") as f:
|
|
1126
|
+
self.assertEqual(json.load(f), design)
|
|
1127
|
+
with open(os.path.join(d, "img_plan.json"), encoding="utf-8") as f:
|
|
1128
|
+
self.assertEqual(json.load(f), img_plan)
|
|
1129
|
+
|
|
1130
|
+
def test_missing_source_assets_json_no_crash(self):
|
|
1131
|
+
d = tempfile.mkdtemp(prefix="srclayout-reconcile-ext-empty-")
|
|
1132
|
+
self.assertEqual(psl.reconcile_source_extensions(d), 0)
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
if __name__ == "__main__":
|
|
1136
|
+
unittest.main(verbosity=2)
|