minimal-agent 0.9.0 → 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 +11 -8
- 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 +2 -1
- package/plugins/workflow-runner/src/agentCommon.js +172 -9
- package/plugins/workflow-runner/src/dslManifest.js +72 -0
- package/plugins/workflow-runner/src/expressions.js +150 -13
- package/plugins/workflow-runner/src/index.js +39 -8
- package/plugins/workflow-runner/src/loader.js +216 -64
- package/plugins/workflow-runner/src/multimodal.js +37 -0
- package/plugins/workflow-runner/src/recovery.js +26 -24
- package/plugins/workflow-runner/src/runner.js +693 -287
- package/plugins/workflow-runner/src/stepExecutors/agent.js +126 -37
- package/plugins/workflow-runner/src/stepExecutors/assert.js +3 -2
- package/plugins/workflow-runner/src/stepExecutors/llm.js +38 -30
- package/plugins/workflow-runner/src/stepExecutors/skill.js +27 -38
- package/plugins/workflow-runner/src/stepExecutors/tool.js +56 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.js +19 -10
- package/plugins/workflow-runner/src/taskIdentity.js +185 -30
- package/plugins/workflow-runner/src/types.js +18 -7
- package/plugins/workflow-runner/src/workflowState.js +355 -55
- 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 +87 -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 +3 -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 +3 -2
- package/src/llm/client.js +293 -206
- package/src/llm/providers/anthropicFormat.js +241 -0
- package/src/llm/providers/openaiFormat.js +190 -0
- package/src/loop.js +211 -27
- 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/pluginRunner.js +40 -0
- package/src/plugins/transcript.js +4 -3
- package/src/prompts/pluginList.js +2 -2
- package/src/prompts/projectInstructions.js +3 -3
- package/src/prompts/promptAssembly.js +133 -109
- package/src/prompts/skillList.js +12 -22
- 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 +19 -17
- package/src/ui/InputBox.js +34 -9
- 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 +345 -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 +9 -2
- package/workflows/book-review-short.yaml +31 -7
- package/workflows/e2e-write-greet.yaml +3 -0
- package/workflows/illustrate.yaml +227 -0
- package/workflows/plan.yaml +437 -207
- package/workflows/ppt.yaml +1659 -0
- package/workflows/ralph-loop.yaml +142 -112
- package/workflows/schema.json +81 -23
- package/workflows/webppt.yaml +496 -0
- package/workflows/word.yaml +710 -0
- package/workflows/youtube-shorts.yaml +38 -10
- package/MINIMAL-TRACE.md +0 -77
- 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/skills/pptx/scripts/office/__pycache__/soffice.cpython-314.pyc +0 -0
- package/src/ui/ProgressPanel.js +0 -54
- package/src/ui/StreamingBlock.js +0 -11
- package/src/ui/ToolStatus.js +0 -11
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
text_readability_pixel.py —— 像素级「文字压图可读性」确定性闸门(ppt 视觉门专用)。
|
|
5
|
+
|
|
6
|
+
为什么需要这个脚本(2026-07-02 逐页体检根因 ⑤)
|
|
7
|
+
====================
|
|
8
|
+
既有对比度检查(export mjs 的 resolveBg → layout_balance 的 lowContrast)只沿 DOM 找 CSS
|
|
9
|
+
background-color——文字压在**背景图片**上时拿到的是页面底色:深色图页刷出大量「白字/浅底 1.05」
|
|
10
|
+
假阳,而真正压图不可读处(标题横穿明暗跨界照片)拿到的也是同一个错误数字。CSS 层面**物理上**
|
|
11
|
+
测不出图片背衬的可读性,必须采真实像素。
|
|
12
|
+
|
|
13
|
+
工作方式
|
|
14
|
+
========
|
|
15
|
+
读 export_pptx_from_html.mjs `--measure-only --with-bg-shots` 的产物:
|
|
16
|
+
- layout-report.json 的 slides[].textRuns(文本原始 px 矩形 + 颜色 + fgParent)
|
|
17
|
+
- slide-NN-background.png(背景态整页截图:隐前景块 + 隐全部文本 = 直接压背景层的文字底下的真实背衬)
|
|
18
|
+
- slide-NN-foreground.png(foreground 态整页截图:隐全部文字、卡片/SVG/渐变/图片原样渲染
|
|
19
|
+
= 卡内/前景块内文字底下的真实背衬。2026-07-03 新增——此前 fgParent 非空的文本被整体跳过,
|
|
20
|
+
真机实测占比高达 81%,深底深字/压卡不可读几乎是检测盲区;CSS 层 resolveBg 也测不准同类场景
|
|
21
|
+
(渐变会跳过继续往上找、读不懂 SVG fill)。两张截图同源同机制,只是隐藏对象不同。)
|
|
22
|
+
对每条文本:`fgParent` 为空 → 从背景截图采样;`fgParent` 非空 → 从 foreground 截图采样
|
|
23
|
+
(该文本本就嵌在某前景块/卡片里,foreground 截图里那块内容原样可见,直接是它的真实背衬,
|
|
24
|
+
无需再猜测卡片颜色)。在其矩形内网格采样像素,逐点算 WCAG 对比度:
|
|
25
|
+
- 失败占比 ≥ FAIL_FRAC_ERROR → ERROR(文字实质不可读)
|
|
26
|
+
- 失败占比 ≥ FAIL_FRAC_WARN → WARNING(部分区域可读性差)
|
|
27
|
+
- 分段局部判定(2026-07-03):矩形横切 6 段逐段判,任一段失败率 ≥70% →
|
|
28
|
+
【文字局部压图不可读】ERROR——治「列表行一半压平板黑边框、另一半净区,整框平均被稀释到
|
|
29
|
+
连 WARNING 都不报」(真机 06 页);窄文本(段内采样列不足)不分段,防小样本误报。
|
|
30
|
+
- 系统性同色归组(2026-07-03):ERROR 里同一文字颜色出现 ≥3 次时追加一行归组提示——真机复盘
|
|
31
|
+
27 处 ERROR 里 15+ 处共享同一 VI 强调色 CSS 变量(`.t-eyebrow`/`.label` 之类多处复用),
|
|
32
|
+
逐条加局部 scrim 的修复策略在有限轮数内修不完;提示指向「改一次共享 CSS 规则/变量能同时
|
|
33
|
+
清零全部同色实例」,纯汇总已收集数据、不新增判断。
|
|
34
|
+
报错带 页 · 文本 · 归一化矩形 · 实测数据 + 修法建议(加 scrim / 移净区 / 换字色)。
|
|
35
|
+
`slide-NN-foreground.png` 缺失(旧 report / 未传 `--with-bg-shots`)时,fgParent 非空的文本
|
|
36
|
+
优雅降级为跳过(与改前行为一致,不新增失败模式)。
|
|
37
|
+
|
|
38
|
+
依赖:Pillow(ppt 工作流 stage_extract 已依赖 PIL,与其同环境)。缺 PIL / 缺截图 / 缺
|
|
39
|
+
textRuns 字段(旧 report)一律**响亮 SKIP 但 exit 0**(fail-open:像素检查是增强层,
|
|
40
|
+
缺依赖不阻断工作流;SKIP 行会进 visual_quality_report 让人看见「本轮没做像素检查」)。
|
|
41
|
+
|
|
42
|
+
用法:python text_readability_pixel.py <workdir> # 含 layout-report.json + slide-NN-background.png
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import argparse
|
|
46
|
+
import json
|
|
47
|
+
import os
|
|
48
|
+
import re
|
|
49
|
+
import sys
|
|
50
|
+
from collections import Counter
|
|
51
|
+
|
|
52
|
+
# 采样与阈值(对照 2026-07-02 真机 deck 23 页逐页标定:02/08/12/15/23 该抓、03/22 像素实测可读不误报、
|
|
53
|
+
# 04/07/13/16/21 章节页装饰序号不误伤)
|
|
54
|
+
SAMPLE_GRID_W = 48 # 矩形内横向最多采样列数
|
|
55
|
+
SAMPLE_GRID_H = 16 # 纵向最多采样行数
|
|
56
|
+
CONTRAST_FLOOR = 3.0 # 放映级大字 AA 阈(与 layout_balance lowContrast 同口径)
|
|
57
|
+
FAIL_FRAC_ERROR = 0.35 # ≥35% 采样点低于阈 → ERROR(文字实质不可读)
|
|
58
|
+
FAIL_FRAC_WARN = 0.15 # ≥15% → WARNING
|
|
59
|
+
MIN_TEXT_LEN = 2 # 单字符/空串不查(装饰符)
|
|
60
|
+
MIN_RECT_PX = 24 # 矩形宽或高小于此不查(图钉级小元素)
|
|
61
|
+
DECOR_NUM_FONT = 96 # 纯数字 ≤3 字符且字号 ≥ 此值 = 章节页水印式装饰序号(低对比是设计意图),豁免
|
|
62
|
+
FG_COVER_SKIP = 0.60 # 文本矩形被某前景块(媒体图/卡)盖住 ≥60% → 真实背衬是该前景块而非背景图,
|
|
63
|
+
# 背景截图采样会错测(slide12 SCENE 文字底下是图块)→ 跳过;文字×前景的碰撞由重叠门负责。
|
|
64
|
+
# 分段局部判定(2026-07-03 真机 06 页根因:列表行一半压在平板黑边框上完全不可读,另一半是净区——
|
|
65
|
+
# 整框平均后失败率被稀释到阈下,连 WARNING 都没报)。把矩形横切 SEG_COUNT 段逐段判:任一段失败率
|
|
66
|
+
# ≥ SEG_FAIL_FRAC 即【文字局部压图不可读】ERROR。段内采样列数不足(窄文本)不做分段判定,防小样本误报。
|
|
67
|
+
SEG_COUNT = 6 # 横向分段数(6 段:单段全挂时整框最低 0.7/6≈11.7%,低于 WARN 线的局部遮挡也抓得到)
|
|
68
|
+
SEG_FAIL_FRAC = 0.70 # 单段失败率 ≥70% → 局部实质不可读
|
|
69
|
+
SEG_MIN_COLS = 6 # 每段至少采样列数(gw ≥ 36 才分段;窄文本整框判定已足够)
|
|
70
|
+
SYSTEMIC_COLOR_MIN_COUNT = 3 # 同一文字颜色的 ERROR 数达此值 → 判定系统性 CSS 变量问题,追加归组提示
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_color(c):
|
|
74
|
+
"""rgb()/rgba()/#hex → (r,g,b) 0-255;解析不了返回 None。"""
|
|
75
|
+
if not isinstance(c, str):
|
|
76
|
+
return None
|
|
77
|
+
m = re.match(r"rgba?\(([^)]+)\)", c.strip())
|
|
78
|
+
if m:
|
|
79
|
+
parts = [p.strip() for p in m.group(1).split(",")]
|
|
80
|
+
try:
|
|
81
|
+
vals = [float(p) for p in parts[:3]]
|
|
82
|
+
except ValueError:
|
|
83
|
+
return None
|
|
84
|
+
if len(vals) >= 3:
|
|
85
|
+
return tuple(min(255, max(0, int(v))) for v in vals)
|
|
86
|
+
return None
|
|
87
|
+
m = re.match(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$", c.strip())
|
|
88
|
+
if m:
|
|
89
|
+
h = m.group(1)
|
|
90
|
+
if len(h) == 3:
|
|
91
|
+
h = "".join(ch * 2 for ch in h)
|
|
92
|
+
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _rel_lum(rgb):
|
|
97
|
+
"""WCAG 相对亮度(0-1,含 gamma 线性化)。"""
|
|
98
|
+
def f(v):
|
|
99
|
+
c = v / 255.0
|
|
100
|
+
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
|
101
|
+
r, g, b = rgb[:3]
|
|
102
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _contrast(l1, l2):
|
|
106
|
+
hi, lo = (l1, l2) if l1 >= l2 else (l2, l1)
|
|
107
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _fg_cover_fraction(run, fg_rects, width, height):
|
|
111
|
+
"""文本 px 矩形被前景块(归一化 rect)盖住的最大比例。"""
|
|
112
|
+
try:
|
|
113
|
+
rx, ry, rw, rh = (float(run[k]) for k in ("x", "y", "w", "h"))
|
|
114
|
+
except (KeyError, TypeError, ValueError):
|
|
115
|
+
return 0.0
|
|
116
|
+
if rw <= 0 or rh <= 0 or width <= 0 or height <= 0:
|
|
117
|
+
return 0.0
|
|
118
|
+
nx, ny, nw, nh = rx / width, ry / height, rw / width, rh / height
|
|
119
|
+
best = 0.0
|
|
120
|
+
for fr in fg_rects:
|
|
121
|
+
if not isinstance(fr, dict):
|
|
122
|
+
continue
|
|
123
|
+
try:
|
|
124
|
+
fx, fy, fw, fh = (float(fr.get(k) or 0.0) for k in ("x", "y", "w", "h"))
|
|
125
|
+
except (TypeError, ValueError):
|
|
126
|
+
continue
|
|
127
|
+
iw = min(nx + nw, fx + fw) - max(nx, fx)
|
|
128
|
+
ih = min(ny + nh, fy + fh) - max(ny, fy)
|
|
129
|
+
if iw > 0 and ih > 0:
|
|
130
|
+
best = max(best, (iw * ih) / (nw * nh))
|
|
131
|
+
return best
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _fmt_rect_px(run, width, height):
|
|
135
|
+
try:
|
|
136
|
+
return (
|
|
137
|
+
f"[x{run['x'] / width:.2f} y{run['y'] / height:.2f} "
|
|
138
|
+
f"w{run['w'] / width:.2f} h{run['h'] / height:.2f}]"
|
|
139
|
+
)
|
|
140
|
+
except (KeyError, TypeError, ZeroDivisionError):
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def check_readability(workdir, report_path=None, verbose=False):
|
|
145
|
+
"""返回 (problems, warnings, skips)。缺依赖/缺产物 → 全空 + skips 说明(fail-open)。"""
|
|
146
|
+
problems, warnings, skips = [], [], []
|
|
147
|
+
# ERROR 项的文字颜色(并行于 problems,供归组提示用;2026-07-03,见下方归组段落)。
|
|
148
|
+
problem_colors = []
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
from PIL import Image # noqa: PLC0415 (可选依赖,缺则响亮降级)
|
|
152
|
+
except ImportError:
|
|
153
|
+
skips.append("Pillow(PIL) 未安装:跳过像素级文字可读性检查(本轮未做像素检查)。")
|
|
154
|
+
return problems, warnings, skips
|
|
155
|
+
|
|
156
|
+
report_path = report_path or os.path.join(workdir, "layout-report.json")
|
|
157
|
+
if not os.path.isfile(report_path):
|
|
158
|
+
skips.append(f"{report_path} 不存在:先跑 export --measure-only --with-bg-shots。")
|
|
159
|
+
return problems, warnings, skips
|
|
160
|
+
try:
|
|
161
|
+
with open(report_path, "r", encoding="utf-8") as f:
|
|
162
|
+
report = json.load(f)
|
|
163
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
164
|
+
skips.append(f"layout-report.json 解析失败:{exc}")
|
|
165
|
+
return problems, warnings, skips
|
|
166
|
+
|
|
167
|
+
width = report.get("width") or 1920
|
|
168
|
+
height = report.get("height") or 1080
|
|
169
|
+
|
|
170
|
+
for slide in report.get("slides") or []:
|
|
171
|
+
idx = slide.get("index")
|
|
172
|
+
label = slide.get("label") or f"slide {idx}"
|
|
173
|
+
runs = slide.get("textRuns")
|
|
174
|
+
if runs is None:
|
|
175
|
+
skips.append(f"{label}:report 无 textRuns 字段(旧 measure 产物),跳过。")
|
|
176
|
+
continue
|
|
177
|
+
shot = os.path.join(workdir, f"slide-{int(idx):02d}-background.png")
|
|
178
|
+
if not os.path.isfile(shot):
|
|
179
|
+
skips.append(f"{label}:缺 {os.path.basename(shot)}(measure 未带 --with-bg-shots),跳过。")
|
|
180
|
+
continue
|
|
181
|
+
try:
|
|
182
|
+
img = Image.open(shot).convert("RGB")
|
|
183
|
+
except OSError as exc:
|
|
184
|
+
skips.append(f"{label}:背景截图打开失败({exc}),跳过。")
|
|
185
|
+
continue
|
|
186
|
+
iw, ih = img.size
|
|
187
|
+
sx = iw / width
|
|
188
|
+
sy = ih / height
|
|
189
|
+
# foreground 态截图(文字隐藏、卡片/SVG/渐变原样渲染):给 fgParent 非空的文本采真实背衬。
|
|
190
|
+
# 缺失(旧 report/未传 --with-bg-shots)→ 这类文本优雅降级跳过,不报 skip(沿用改前行为,避免刷屏)。
|
|
191
|
+
fg_shot = os.path.join(workdir, f"slide-{int(idx):02d}-foreground.png")
|
|
192
|
+
fg_img = None
|
|
193
|
+
if os.path.isfile(fg_shot):
|
|
194
|
+
try:
|
|
195
|
+
fg_img = Image.open(fg_shot).convert("RGB")
|
|
196
|
+
except OSError:
|
|
197
|
+
fg_img = None
|
|
198
|
+
balance = slide.get("balance") or {}
|
|
199
|
+
# 前景块矩形(媒体图 + 卡/底板):fgParent=null 的文本被它们盖住时真实背衬不是背景图
|
|
200
|
+
fg_rects = list(balance.get("mediaRects") or [])
|
|
201
|
+
fg_rects += [c.get("rect") for c in (balance.get("cards") or []) if isinstance(c, dict)]
|
|
202
|
+
|
|
203
|
+
for run in runs:
|
|
204
|
+
text = (run.get("text") or "").strip()
|
|
205
|
+
if len(text) < MIN_TEXT_LEN:
|
|
206
|
+
continue
|
|
207
|
+
if text.isdigit() and len(text) <= 3 and (run.get("fontSize") or 0) >= DECOR_NUM_FONT:
|
|
208
|
+
continue # 章节页水印式大号序号(01-05):低对比是设计意图,不是缺陷
|
|
209
|
+
has_fg_parent = bool(run.get("fgParent"))
|
|
210
|
+
if has_fg_parent:
|
|
211
|
+
if fg_img is None:
|
|
212
|
+
continue # 无 foreground 截图,优雅降级跳过
|
|
213
|
+
sample_img, samp_sx, samp_sy = fg_img, fg_img.size[0] / width, fg_img.size[1] / height
|
|
214
|
+
else:
|
|
215
|
+
if fg_rects and _fg_cover_fraction(run, fg_rects, width, height) >= FG_COVER_SKIP:
|
|
216
|
+
continue # 底下垫着前景图块/卡但 DOM 未挂 fgParent:真实背衬非背景图;碰撞类问题由重叠门负责
|
|
217
|
+
sample_img, samp_sx, samp_sy = img, sx, sy
|
|
218
|
+
color = _parse_color(run.get("color"))
|
|
219
|
+
if color is None:
|
|
220
|
+
continue
|
|
221
|
+
try:
|
|
222
|
+
x, y, w, h = (float(run[k]) for k in ("x", "y", "w", "h"))
|
|
223
|
+
except (KeyError, TypeError, ValueError):
|
|
224
|
+
continue
|
|
225
|
+
if w < MIN_RECT_PX or h < MIN_RECT_PX:
|
|
226
|
+
continue
|
|
227
|
+
# 裁剪到画布内(越界文本归超画布门管);缩放系数按实际采样图自身尺寸算(foreground/background
|
|
228
|
+
# 截图理论同尺寸,但不假设——各自图自己算最稳妥)。
|
|
229
|
+
x0 = max(0.0, x) * samp_sx
|
|
230
|
+
y0 = max(0.0, y) * samp_sy
|
|
231
|
+
x1 = min(width, x + w) * samp_sx
|
|
232
|
+
y1 = min(height, y + h) * samp_sy
|
|
233
|
+
if x1 - x0 < 2 or y1 - y0 < 2:
|
|
234
|
+
continue
|
|
235
|
+
crop = sample_img.crop((int(x0), int(y0), int(x1), int(y1)))
|
|
236
|
+
# 缩到采样网格:几百个点足够统计,帧级快
|
|
237
|
+
gw = max(1, min(SAMPLE_GRID_W, crop.width))
|
|
238
|
+
gh = max(1, min(SAMPLE_GRID_H, crop.height))
|
|
239
|
+
crop = crop.resize((gw, gh))
|
|
240
|
+
text_lum = _rel_lum(color)
|
|
241
|
+
fails = 0
|
|
242
|
+
worst = None
|
|
243
|
+
total = gw * gh
|
|
244
|
+
col_fails = [0] * gw # 逐列失败计数(行主序 getdata:i % gw = 列号),供分段局部判定
|
|
245
|
+
for i, px in enumerate(crop.getdata()):
|
|
246
|
+
ratio = _contrast(text_lum, _rel_lum(px))
|
|
247
|
+
if ratio < CONTRAST_FLOOR:
|
|
248
|
+
fails += 1
|
|
249
|
+
col_fails[i % gw] += 1
|
|
250
|
+
if worst is None or ratio < worst:
|
|
251
|
+
worst = ratio
|
|
252
|
+
frac = fails / total if total else 0.0
|
|
253
|
+
# 分段局部判定:任一横段失败率 ≥ SEG_FAIL_FRAC = 局部实质不可读(整框平均会稀释它)。
|
|
254
|
+
seg_hit = None
|
|
255
|
+
if gw >= SEG_MIN_COLS * SEG_COUNT and gh > 0:
|
|
256
|
+
for s in range(SEG_COUNT):
|
|
257
|
+
c0 = s * gw // SEG_COUNT
|
|
258
|
+
c1 = (s + 1) * gw // SEG_COUNT
|
|
259
|
+
seg_total = (c1 - c0) * gh
|
|
260
|
+
if seg_total <= 0:
|
|
261
|
+
continue
|
|
262
|
+
seg_frac = sum(col_fails[c0:c1]) / seg_total
|
|
263
|
+
if seg_frac >= SEG_FAIL_FRAC:
|
|
264
|
+
seg_hit = (s + 1, seg_frac)
|
|
265
|
+
break
|
|
266
|
+
if verbose:
|
|
267
|
+
print(f" {label} · 「{text[:16]}」 fail={frac*100:.0f}% worst={worst or 0:.2f}"
|
|
268
|
+
+ (f" seg{seg_hit[0]}={seg_hit[1]*100:.0f}%" if seg_hit else ""))
|
|
269
|
+
fix_hint = (
|
|
270
|
+
f" |可参考修法(非强制):文字下加局部 scrim/暗角底板(紧贴文字,禁大面积白卡)"
|
|
271
|
+
f" → 或把文字移动到背景净区 → 或换字色(深底白字/浅底深墨字)"
|
|
272
|
+
)
|
|
273
|
+
if frac >= FAIL_FRAC_WARN:
|
|
274
|
+
seg_note = (
|
|
275
|
+
f";左起第 {seg_hit[0]}/{SEG_COUNT} 段 {seg_hit[1]*100:.0f}% 局部实质不可读"
|
|
276
|
+
if seg_hit and frac < FAIL_FRAC_ERROR else ""
|
|
277
|
+
)
|
|
278
|
+
msg = (
|
|
279
|
+
f"【文字压图不可读】{label} · 「{text[:16]}」{_fmt_rect_px(run, width, height)}"
|
|
280
|
+
f" · 实测 {frac*100:.0f}% 采样点对比度 <{CONTRAST_FLOOR}(最差 {worst:.2f},文字 {run.get('color')}{seg_note})"
|
|
281
|
+
+ fix_hint
|
|
282
|
+
)
|
|
283
|
+
if frac >= FAIL_FRAC_ERROR or seg_hit:
|
|
284
|
+
problems.append(msg)
|
|
285
|
+
problem_colors.append(run.get("color"))
|
|
286
|
+
else:
|
|
287
|
+
warnings.append(msg)
|
|
288
|
+
elif seg_hit:
|
|
289
|
+
problems.append(
|
|
290
|
+
f"【文字局部压图不可读】{label} · 「{text[:16]}」{_fmt_rect_px(run, width, height)}"
|
|
291
|
+
f" · 左起第 {seg_hit[0]}/{SEG_COUNT} 段 {seg_hit[1]*100:.0f}% 采样点对比度 <{CONTRAST_FLOOR}"
|
|
292
|
+
f"(整框仅 {frac*100:.0f}%——局部遮挡被整框平均稀释,文字 {run.get('color')})"
|
|
293
|
+
+ fix_hint
|
|
294
|
+
)
|
|
295
|
+
problem_colors.append(run.get("color"))
|
|
296
|
+
|
|
297
|
+
# 系统性同色归组提示(2026-07-03 真机复盘:27 处 ERROR 里 15+ 处共享同一 CSS 变量取值,
|
|
298
|
+
# 逐条加局部 scrim 的修复策略在有限轮数内根本修不完;真正的修法是改一次共享 CSS 规则/变量)。
|
|
299
|
+
# 纯汇总已收集的 problem_colors,不新增判断/猜测——只是把「这些问题其实是一件事」的信号显式标出来。
|
|
300
|
+
color_counts = Counter(c for c in problem_colors if c)
|
|
301
|
+
for color, count in color_counts.most_common():
|
|
302
|
+
if count >= SYSTEMIC_COLOR_MIN_COUNT:
|
|
303
|
+
problems.append(
|
|
304
|
+
f"⚠️ {count} 处文字压图问题共享同一文字颜色 {color}:疑似同一 CSS 变量/class 系统性问题"
|
|
305
|
+
f"(如 `.t-eyebrow`/`.label` 之类被多处复用的强调色),建议**一次性改共享 CSS 规则/变量**"
|
|
306
|
+
f"(改深该颜色,或给该 class 统一加色块/半透明底),而非逐条加局部 scrim——"
|
|
307
|
+
f"逐条修在有限轮数内修不完,改一次能同时清零全部同色实例。"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return problems, warnings, skips
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def main() -> int:
|
|
314
|
+
parser = argparse.ArgumentParser(
|
|
315
|
+
description="像素级文字压图可读性闸门(读 measure-only --with-bg-shots 产物;缺依赖响亮 SKIP 不阻断)"
|
|
316
|
+
)
|
|
317
|
+
parser.add_argument("workdir", help="含 layout-report.json + slide-NN-background.png 的目录")
|
|
318
|
+
parser.add_argument("--report", help="layout-report.json 路径(默认 <workdir>/layout-report.json)")
|
|
319
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="打印每条文本的采样明细")
|
|
320
|
+
args = parser.parse_args()
|
|
321
|
+
|
|
322
|
+
problems, warnings, skips = check_readability(args.workdir, report_path=args.report, verbose=args.verbose)
|
|
323
|
+
|
|
324
|
+
for s in skips:
|
|
325
|
+
print("SKIP: " + s)
|
|
326
|
+
for w in warnings:
|
|
327
|
+
print("WARNING: " + w)
|
|
328
|
+
if problems:
|
|
329
|
+
print(f"文字压图可读性检查 FAILED —— {len(problems)} 处文字实质不可读:")
|
|
330
|
+
for p in problems:
|
|
331
|
+
print(" - " + p)
|
|
332
|
+
return 1
|
|
333
|
+
print("文字压图可读性检查 PASSED"
|
|
334
|
+
+ ("(部分页跳过,见 SKIP)" if skips else "")
|
|
335
|
+
+ f"(WARNING {len(warnings)} 条)")
|
|
336
|
+
return 0
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
if __name__ == "__main__":
|
|
340
|
+
sys.exit(main())
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
text_readability_pixel_test.py —— 像素级文字可读性门的回归锚(stdlib unittest + Pillow)。
|
|
5
|
+
|
|
6
|
+
用 PIL 合成「左黑右白」背景图 + 手搓 layout-report.json,锁定:
|
|
7
|
+
- 白字压亮区 → ERROR;白字压暗区 → 通过(像素实测,不是 CSS 猜测);
|
|
8
|
+
- fgParent 文本跳过(卡内文字背衬是卡);
|
|
9
|
+
- 章节页水印式大号纯数字序号豁免;
|
|
10
|
+
- 文本被前景块盖住 ≥60% 跳过(真实背衬非背景图);
|
|
11
|
+
- 缺截图 / 旧 report 无 textRuns → SKIP 不报错(fail-open)。
|
|
12
|
+
|
|
13
|
+
运行:python skills/ui-design/scripts/text_readability_pixel_test.py
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
import unittest
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from PIL import Image
|
|
26
|
+
HAS_PIL = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
HAS_PIL = False
|
|
29
|
+
|
|
30
|
+
from text_readability_pixel import check_readability # noqa: E402
|
|
31
|
+
|
|
32
|
+
W, H = 640, 360 # 测试画布(report 声明尺寸 = 截图尺寸,省缩放歧义)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(text="标题文字", x=0, y=0, w=W, h=64, color="rgb(255, 255, 255)",
|
|
36
|
+
font_size=40.0, fg_parent=None):
|
|
37
|
+
return {"text": text, "x": x, "y": y, "w": w, "h": h, "fontSize": font_size,
|
|
38
|
+
"color": color, "bgColor": None, "bgSolid": False, "fgParent": fg_parent}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@unittest.skipUnless(HAS_PIL, "Pillow 未安装(真实环境该门会响亮 SKIP,见 test_missing 系列)")
|
|
42
|
+
class TextReadabilityPixelTest(unittest.TestCase):
|
|
43
|
+
def setUp(self):
|
|
44
|
+
self.dir = tempfile.mkdtemp()
|
|
45
|
+
|
|
46
|
+
def tearDown(self):
|
|
47
|
+
for f in os.listdir(self.dir):
|
|
48
|
+
try:
|
|
49
|
+
os.remove(os.path.join(self.dir, f))
|
|
50
|
+
except OSError:
|
|
51
|
+
pass
|
|
52
|
+
os.rmdir(self.dir)
|
|
53
|
+
|
|
54
|
+
def _write(self, slides, shots=True, fg_shots=False, fg_inverted=False):
|
|
55
|
+
"""slides: [(textRuns, balance_extra)];每页配一张左黑右白的背景图。
|
|
56
|
+
fg_shots=True 时额外落一张 foreground 截图(fg_inverted=True 时明暗左右互换,
|
|
57
|
+
用来验证 fgParent 文本确实采样自 foreground 图、不是背景图)。"""
|
|
58
|
+
report = {"mode": "hybrid", "width": W, "height": H, "slides": []}
|
|
59
|
+
for i, (runs, balance_extra) in enumerate(slides, start=1):
|
|
60
|
+
balance = {"mediaRects": [], "cards": []}
|
|
61
|
+
balance.update(balance_extra or {})
|
|
62
|
+
report["slides"].append(
|
|
63
|
+
{"index": i, "label": f"{i:02d} 测试页", "balance": balance, "textRuns": runs})
|
|
64
|
+
if shots:
|
|
65
|
+
img = Image.new("RGB", (W, H), (250, 250, 250)) # 右/整体亮
|
|
66
|
+
dark = Image.new("RGB", (W // 2, H), (10, 12, 20)) # 左半暗
|
|
67
|
+
img.paste(dark, (0, 0))
|
|
68
|
+
img.save(os.path.join(self.dir, f"slide-{i:02d}-background.png"))
|
|
69
|
+
if fg_shots:
|
|
70
|
+
# 默认与背景截图同花色(左暗右亮);fg_inverted=True 时左右互换,
|
|
71
|
+
# 用来证明采样确实来自 foreground 图(背景图此处会给出相反的可读性结论)。
|
|
72
|
+
left, right = ((250, 250, 250), (10, 12, 20)) if fg_inverted else ((10, 12, 20), (250, 250, 250))
|
|
73
|
+
fg = Image.new("RGB", (W, H), right)
|
|
74
|
+
fg.paste(Image.new("RGB", (W // 2, H), left), (0, 0))
|
|
75
|
+
fg.save(os.path.join(self.dir, f"slide-{i:02d}-foreground.png"))
|
|
76
|
+
with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(report, f, ensure_ascii=False)
|
|
78
|
+
|
|
79
|
+
def test_white_text_on_bright_area_is_error(self):
|
|
80
|
+
# 白字整框压在右侧亮区 → 100% 采样点对比度 <3 → ERROR。
|
|
81
|
+
self._write([([run(x=W // 2, w=W // 2)], None)])
|
|
82
|
+
problems, warnings, skips = check_readability(self.dir)
|
|
83
|
+
self.assertTrue(any("压图不可读" in p for p in problems), (problems, warnings, skips))
|
|
84
|
+
|
|
85
|
+
def test_white_text_on_dark_area_passes(self):
|
|
86
|
+
# 白字压左侧暗区 → 像素实测可读(CSS 检查在这类页会误报 1.05,这里是真采样)。
|
|
87
|
+
self._write([([run(x=0, w=W // 2)], None)])
|
|
88
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
89
|
+
self.assertEqual(problems, [], problems)
|
|
90
|
+
self.assertEqual(warnings, [], warnings)
|
|
91
|
+
|
|
92
|
+
def test_partial_bright_tail_escalates_via_segment(self):
|
|
93
|
+
# 框 x80-400、暗区 0-320 → 亮区占框尾部 25%:整框 25%(原本只 WARNING),
|
|
94
|
+
# 但尾段失败率 100% ≥70% → 分段判定升 ERROR(2026-07-03:局部遮挡不再被整框平均稀释)。
|
|
95
|
+
self._write([([run(x=W // 8, w=W // 2)], None)])
|
|
96
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
97
|
+
hits = [p for p in problems if "压图不可读" in p]
|
|
98
|
+
self.assertEqual(len(hits), 1, (problems, warnings))
|
|
99
|
+
self.assertIn("局部实质不可读", hits[0])
|
|
100
|
+
|
|
101
|
+
def test_bottom_stripe_stays_warning(self):
|
|
102
|
+
# 亮区是横向贯通的底部条纹(每列失败 25% 均匀分布)→ 无单段 ≥70% → 仍 WARNING 不升级。
|
|
103
|
+
report = {"mode": "hybrid", "width": W, "height": H, "slides": [
|
|
104
|
+
{"index": 1, "label": "01 测试页", "balance": {"mediaRects": [], "cards": []},
|
|
105
|
+
"textRuns": [run(x=0, w=W, h=160)]}]}
|
|
106
|
+
with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
|
|
107
|
+
json.dump(report, f, ensure_ascii=False)
|
|
108
|
+
img = Image.new("RGB", (W, H), (10, 12, 20)) # 整体暗(白字可读)
|
|
109
|
+
img.paste(Image.new("RGB", (W, 40), (250, 250, 250)), (0, 120)) # 框内底部 40/160=25% 亮条
|
|
110
|
+
img.save(os.path.join(self.dir, "slide-01-background.png"))
|
|
111
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
112
|
+
self.assertEqual(problems, [], problems)
|
|
113
|
+
self.assertTrue(any("压图不可读" in w for w in warnings), warnings)
|
|
114
|
+
|
|
115
|
+
def test_localized_below_warn_threshold_caught_by_segment(self):
|
|
116
|
+
# 亮区只占整框 ~12.5%(低于 15% WARN 线,改前完全不报),但集中在第 6 段(~86% 段失败率)
|
|
117
|
+
# → 【文字局部压图不可读】ERROR。正是真机 06 页「列表行局部压平板黑边框」的形态。
|
|
118
|
+
report = {"mode": "hybrid", "width": W, "height": H, "slides": [
|
|
119
|
+
{"index": 1, "label": "01 测试页", "balance": {"mediaRects": [], "cards": []},
|
|
120
|
+
"textRuns": [run(x=0, w=W, h=64)]}]}
|
|
121
|
+
with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
|
|
122
|
+
json.dump(report, f, ensure_ascii=False)
|
|
123
|
+
img = Image.new("RGB", (W, H), (10, 12, 20)) # 整体暗
|
|
124
|
+
img.paste(Image.new("RGB", (80, H), (250, 250, 250)), (548, 0)) # 亮块 x548-628(第 6 段内)
|
|
125
|
+
img.save(os.path.join(self.dir, "slide-01-background.png"))
|
|
126
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
127
|
+
hits = [p for p in problems if "文字局部压图不可读" in p]
|
|
128
|
+
self.assertEqual(len(hits), 1, (problems, warnings))
|
|
129
|
+
self.assertIn("第 6/6 段", hits[0])
|
|
130
|
+
self.assertEqual(warnings, [], warnings)
|
|
131
|
+
|
|
132
|
+
def test_card_text_skipped_without_foreground_shot(self):
|
|
133
|
+
# 卡内文字(fgParent 非空)+ 无 foreground 截图(旧 report/未传 --with-bg-shots)→ 优雅降级跳过。
|
|
134
|
+
self._write([([run(x=W // 2, w=W // 2, fg_parent="fg-0-1")], None)])
|
|
135
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
136
|
+
self.assertEqual(problems, [], problems)
|
|
137
|
+
self.assertEqual(warnings, [], warnings)
|
|
138
|
+
|
|
139
|
+
def test_card_text_sampled_from_foreground_shot(self):
|
|
140
|
+
# 2026-07-03:有 foreground 截图时卡内文字不再跳过,从它采样——白字压 foreground 图的亮区 → ERROR。
|
|
141
|
+
# 背景截图右半同样是亮区(同花色未 inverted),如果误采背景图结论一致,不能单独证明用对了图;
|
|
142
|
+
# 用 test_card_text_uses_foreground_not_background 补一个明暗相反的对照用例来坐实。
|
|
143
|
+
self._write([([run(x=W // 2, w=W // 2, fg_parent="fg-0-1")], None)], fg_shots=True)
|
|
144
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
145
|
+
self.assertTrue(any("压图不可读" in p for p in problems), (problems, warnings))
|
|
146
|
+
|
|
147
|
+
def test_card_text_uses_foreground_not_background(self):
|
|
148
|
+
# foreground 截图与背景截图明暗互换:文字位置在背景图是「暗区可读」,在 foreground 图是「亮区不可读」。
|
|
149
|
+
# 若采样源用错(误采背景图)本例会通过;实际应因采了 foreground 图而报 ERROR,坐实采样源正确。
|
|
150
|
+
self._write([([run(x=0, w=W // 2, fg_parent="fg-0-1")], None)], fg_shots=True, fg_inverted=True)
|
|
151
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
152
|
+
self.assertTrue(any("压图不可读" in p for p in problems), (problems, warnings))
|
|
153
|
+
|
|
154
|
+
def test_card_text_readable_via_foreground_shot(self):
|
|
155
|
+
# 卡内文字压在 foreground 图的暗区 → 像素实测可读,不报。
|
|
156
|
+
self._write([([run(x=0, w=W // 2, fg_parent="fg-0-1")], None)], fg_shots=True)
|
|
157
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
158
|
+
self.assertEqual(problems, [], problems)
|
|
159
|
+
self.assertEqual(warnings, [], warnings)
|
|
160
|
+
|
|
161
|
+
def test_decorative_numeral_exempt(self):
|
|
162
|
+
# 章节页水印式大号序号(纯数字 ≤3 字符、字号 ≥96):低对比是设计意图。
|
|
163
|
+
self._write([([run(text="01", x=W // 2, w=200, h=180, font_size=170.0)], None)])
|
|
164
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
165
|
+
self.assertEqual(problems, [], problems)
|
|
166
|
+
self.assertEqual(warnings, [], warnings)
|
|
167
|
+
|
|
168
|
+
def test_systemic_color_grouping_hint_at_three_plus(self):
|
|
169
|
+
# 2026-07-03:3 处 ERROR 共享同一文字颜色 → 追加归组提示(真机复盘:15+ 处共享
|
|
170
|
+
# 同一 VI 强调色变量,逐条加 scrim 在有限轮数内修不完,需要一次性改共享 CSS 规则)。
|
|
171
|
+
runs = [
|
|
172
|
+
run(text=f"标题{i}", x=W // 2, y=i * 60, w=W // 2, h=50)
|
|
173
|
+
for i in range(3)
|
|
174
|
+
] # 同色(默认白字)、都压在右侧亮区 → 3 处 ERROR
|
|
175
|
+
self._write([(runs, None)])
|
|
176
|
+
problems, _w, _s = check_readability(self.dir)
|
|
177
|
+
hits = [p for p in problems if "共享同一文字颜色" in p]
|
|
178
|
+
self.assertEqual(len(hits), 1, problems)
|
|
179
|
+
self.assertIn("rgb(255, 255, 255)", hits[0])
|
|
180
|
+
self.assertIn("3 处", hits[0])
|
|
181
|
+
|
|
182
|
+
def test_no_grouping_hint_below_threshold(self):
|
|
183
|
+
# 仅 2 处同色 ERROR(未达 SYSTEMIC_COLOR_MIN_COUNT=3)→ 不追加归组提示。
|
|
184
|
+
runs = [run(text=f"标题{i}", x=W // 2, y=i * 60, w=W // 2, h=50) for i in range(2)]
|
|
185
|
+
self._write([(runs, None)])
|
|
186
|
+
problems, _w, _s = check_readability(self.dir)
|
|
187
|
+
self.assertFalse(any("共享同一文字颜色" in p for p in problems), problems)
|
|
188
|
+
|
|
189
|
+
def test_fg_covered_text_skipped(self):
|
|
190
|
+
# 文本被前景媒体块盖住 ≥60% → 真实背衬是该图块(slide12 SCENE 案)→ 跳过。
|
|
191
|
+
media = [{"x": 0.5, "y": 0.0, "w": 0.5, "h": 1.0}]
|
|
192
|
+
self._write([([run(x=W // 2, w=W // 2)], {"mediaRects": media})])
|
|
193
|
+
problems, warnings, _s = check_readability(self.dir)
|
|
194
|
+
self.assertEqual(problems, [], problems)
|
|
195
|
+
self.assertEqual(warnings, [], warnings)
|
|
196
|
+
|
|
197
|
+
def test_missing_shot_is_skip_not_error(self):
|
|
198
|
+
self._write([([run()], None)], shots=False)
|
|
199
|
+
problems, _w, skips = check_readability(self.dir)
|
|
200
|
+
self.assertEqual(problems, [], problems)
|
|
201
|
+
self.assertTrue(any("缺" in s for s in skips), skips)
|
|
202
|
+
|
|
203
|
+
def test_old_report_without_textruns_is_skip(self):
|
|
204
|
+
report = {"mode": "hybrid", "width": W, "height": H,
|
|
205
|
+
"slides": [{"index": 1, "label": "01 旧页", "balance": {}}]}
|
|
206
|
+
with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
|
|
207
|
+
json.dump(report, f, ensure_ascii=False)
|
|
208
|
+
problems, _w, skips = check_readability(self.dir)
|
|
209
|
+
self.assertEqual(problems, [], problems)
|
|
210
|
+
self.assertTrue(any("textRuns" in s for s in skips), skips)
|
|
211
|
+
|
|
212
|
+
def test_missing_report_is_skip(self):
|
|
213
|
+
problems, _w, skips = check_readability(self.dir)
|
|
214
|
+
self.assertEqual(problems, [], problems)
|
|
215
|
+
self.assertTrue(skips, skips)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
unittest.main(verbosity=2)
|