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,368 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
image_stats.py —— 图像信息统计:确定性算出图片的客观量化指标。
|
|
5
|
+
|
|
6
|
+
为什么需要这个脚本?
|
|
7
|
+
====================
|
|
8
|
+
minimal-agent 内核已支持多模态读图,模型能「看懂」图片;但「看懂」≠「量化」。
|
|
9
|
+
宽高 / 分辨率 / 平均颜色 / 色调 / 直方图这类**具体数字**靠模型目测既不准也不稳——
|
|
10
|
+
它会凭印象说「偏暖、大概 1920×1080」,却说不出平均 RGB、主色占比、亮度方差。
|
|
11
|
+
这些都是确定性计算,应交脚本(与 image_plan_lint.py / layout_balance.py /
|
|
12
|
+
deck_pages_check.py 同哲学:机械活收归脚本,不烧 LLM、零幻觉)。
|
|
13
|
+
|
|
14
|
+
算什么?
|
|
15
|
+
========
|
|
16
|
+
对每张图片:
|
|
17
|
+
- 基本:format / mode / 文件大小 / 是否带 alpha
|
|
18
|
+
- 尺寸:width×height / 宽高比 / 朝向(landscape/portrait/square) / 百万像素
|
|
19
|
+
- 分辨率:DPI(无元数据则 null)
|
|
20
|
+
- 平均颜色:平均 RGB + hex + 逐通道
|
|
21
|
+
- 亮度/对比:感知亮度均值 / 标准差(≈对比度) / min / max
|
|
22
|
+
- 色调:HSV 的饱和度加权环形平均 hue + 平均 sat/val + warm/cool/neutral 分类 + 是否灰阶
|
|
23
|
+
- 主色:quantize 取 top-N 主色 + 各自 hex 与占比
|
|
24
|
+
- 直方图:R/G/B 三通道 + 亮度,默认 16 bin 分箱(--bins 256 出全直方图)
|
|
25
|
+
|
|
26
|
+
只依赖 Pillow + 标准库(json/argparse/os/sys/math)——**不引入 numpy**
|
|
27
|
+
(全仓库未用;PIL 自带 histogram()/ImageStat/quantize()/convert('HSV') 已够)。
|
|
28
|
+
|
|
29
|
+
诚实边界
|
|
30
|
+
========
|
|
31
|
+
- 逐像素统计(色调/主色/直方图)前把分析副本缩略到最长边 ≤512px 提速;
|
|
32
|
+
尺寸/DPI/format 仍报**原图**真实值,不受缩略影响。
|
|
33
|
+
- 平均色/色调基于 RGB 通道,**忽略 alpha**(透明像素的 RGB 也计入);带 alpha 的图另有 has_alpha 标注。
|
|
34
|
+
- 色调环形平均按饱和度加权(避免大片灰色噪声拉偏 hue);饱和度极低时判 grayscale、hue 记 null。
|
|
35
|
+
- warm/cool/neutral 是色环的粗分类启发式(见 classify_tone),不是色彩学严格定义。
|
|
36
|
+
|
|
37
|
+
退出码:0 = 正常(至少统计到 1 张,或 --scan 未发现图片);1 = 指定了图片但一张都读不了;2 = 参数错。
|
|
38
|
+
单张图片读失败 → 打到 stderr 跳过、不影响其余。
|
|
39
|
+
|
|
40
|
+
用法
|
|
41
|
+
====
|
|
42
|
+
python image_stats.py a.png b.jpg # 文本报告
|
|
43
|
+
python image_stats.py ./shots --scan # 扫目录下所有受支持图片(非递归)
|
|
44
|
+
python image_stats.py a.png --json # 机读 JSON(工作流/宿主消费,stdout 只有 JSON)
|
|
45
|
+
python image_stats.py a.png --bins 256 --top 8 # 全直方图 + 取 8 个主色
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import json
|
|
50
|
+
import math
|
|
51
|
+
import os
|
|
52
|
+
import sys
|
|
53
|
+
|
|
54
|
+
from PIL import Image, ImageStat
|
|
55
|
+
|
|
56
|
+
# --scan 扫目录时识别的图片后缀;直接传文件路径则不受此限、能开就统计。
|
|
57
|
+
SUPPORTED_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tif", ".tiff"}
|
|
58
|
+
|
|
59
|
+
# 逐像素统计前缩略到的最长边像素(仅为提速,不改原图、不影响尺寸/DPI 报告)。
|
|
60
|
+
ANALYSIS_MAX_SIDE = 512
|
|
61
|
+
|
|
62
|
+
# 朝向阈值:与 extract_source_assets.py 保持一致,避免两套脚本对同一图判断打架。
|
|
63
|
+
LANDSCAPE_MIN = 1.25
|
|
64
|
+
PORTRAIT_MAX = 0.80
|
|
65
|
+
|
|
66
|
+
# 判定灰阶的平均饱和度上限(0~1)。低于此视为基本无彩色。
|
|
67
|
+
GRAYSCALE_SAT_MAX = 0.08
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def human_size(n: int) -> str:
|
|
71
|
+
"""字节数 → 人话(B/KB/MB)。"""
|
|
72
|
+
f = float(n)
|
|
73
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
74
|
+
if f < 1024 or unit == "GB":
|
|
75
|
+
return f"{f:.0f} {unit}" if unit == "B" else f"{f:.1f} {unit}"
|
|
76
|
+
f /= 1024
|
|
77
|
+
return f"{f:.1f} GB"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_hex(rgb) -> str:
|
|
81
|
+
r, g, b = (int(round(c)) for c in rgb[:3])
|
|
82
|
+
return f"#{r:02X}{g:02X}{b:02X}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def classify_orientation(ar: float) -> str:
|
|
86
|
+
if ar >= LANDSCAPE_MIN:
|
|
87
|
+
return "landscape"
|
|
88
|
+
if ar <= PORTRAIT_MAX:
|
|
89
|
+
return "portrait"
|
|
90
|
+
return "square"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def classify_tone(hue_deg, is_grayscale: bool) -> str:
|
|
94
|
+
"""色环粗分类(启发式):grayscale / warm / cool / neutral。"""
|
|
95
|
+
if is_grayscale or hue_deg is None:
|
|
96
|
+
return "grayscale"
|
|
97
|
+
h = hue_deg % 360
|
|
98
|
+
if h < 45 or h >= 315:
|
|
99
|
+
return "warm" # 红/橙 与 品红→红
|
|
100
|
+
if 90 <= h < 270:
|
|
101
|
+
return "cool" # 绿/青/蓝/紫
|
|
102
|
+
return "neutral" # 黄(45~90) 与 紫红(270~315) 过渡带
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def bin_histogram(counts, bins: int):
|
|
106
|
+
"""把 256 桶的单通道直方图重分箱到 bins 个桶。"""
|
|
107
|
+
if bins >= 256:
|
|
108
|
+
return list(counts)
|
|
109
|
+
out = [0] * bins
|
|
110
|
+
width = 256.0 / bins
|
|
111
|
+
for i, c in enumerate(counts):
|
|
112
|
+
out[min(bins - 1, int(i / width))] += c
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def compute_tone(hsv: Image.Image):
|
|
117
|
+
"""对 HSV 缩略图算「饱和度加权环形平均 hue」+ 平均 sat/val。
|
|
118
|
+
|
|
119
|
+
线性平均 hue 会因为红色在 0°/360° 处回绕而出错;这里用 sin/cos 向量和做环形
|
|
120
|
+
平均,并以饱和度为权重(灰色像素 hue 无意义、不该参与)。
|
|
121
|
+
"""
|
|
122
|
+
sin_sum = 0.0
|
|
123
|
+
cos_sum = 0.0
|
|
124
|
+
sat_weight = 0.0
|
|
125
|
+
sat_total = 0.0
|
|
126
|
+
val_total = 0.0
|
|
127
|
+
n = 0
|
|
128
|
+
for h, s, v in iter_pixels(hsv): # PIL HSV:每通道 0~255
|
|
129
|
+
n += 1
|
|
130
|
+
sat_total += s
|
|
131
|
+
val_total += v
|
|
132
|
+
if s > 0:
|
|
133
|
+
ang = (h / 256.0) * 2 * math.pi
|
|
134
|
+
sin_sum += math.sin(ang) * s
|
|
135
|
+
cos_sum += math.cos(ang) * s
|
|
136
|
+
sat_weight += s
|
|
137
|
+
if n == 0:
|
|
138
|
+
return {"hue_deg": None, "saturation": 0.0, "value": 0.0}
|
|
139
|
+
sat_mean = (sat_total / n) / 255.0
|
|
140
|
+
val_mean = (val_total / n) / 255.0
|
|
141
|
+
if sat_weight > 0 and (sin_sum or cos_sum):
|
|
142
|
+
hue_deg = math.degrees(math.atan2(sin_sum, cos_sum)) % 360.0
|
|
143
|
+
else:
|
|
144
|
+
hue_deg = None
|
|
145
|
+
return {
|
|
146
|
+
"hue_deg": None if hue_deg is None else round(hue_deg, 1),
|
|
147
|
+
"saturation": round(sat_mean, 4),
|
|
148
|
+
"value": round(val_mean, 4),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def iter_pixels(image: Image.Image):
|
|
153
|
+
"""兼容 Pillow 新旧版本的像素迭代。
|
|
154
|
+
|
|
155
|
+
Pillow 13 起 Image.getdata() 弃用;新版提供 get_flattened_data()。这里优先用
|
|
156
|
+
新 API,旧环境 fallback,避免为了一个像素迭代引入 numpy。
|
|
157
|
+
"""
|
|
158
|
+
getter = getattr(image, "get_flattened_data", None)
|
|
159
|
+
if callable(getter):
|
|
160
|
+
return getter()
|
|
161
|
+
return image.getdata()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def dominant_colors(rgb: Image.Image, top: int):
|
|
165
|
+
"""quantize 取 top-N 主色(含占比)。颜色数不足时自然返回更少。"""
|
|
166
|
+
q = rgb.quantize(colors=max(1, top), method=Image.Quantize.MEDIANCUT)
|
|
167
|
+
palette = q.getpalette() or []
|
|
168
|
+
counts = q.getcolors() or [] # [(count, palette_index), ...]
|
|
169
|
+
total = sum(c for c, _ in counts) or 1
|
|
170
|
+
counts.sort(reverse=True)
|
|
171
|
+
out = []
|
|
172
|
+
for count, idx in counts[:top]:
|
|
173
|
+
r, g, b = palette[idx * 3 : idx * 3 + 3]
|
|
174
|
+
out.append(
|
|
175
|
+
{"hex": to_hex((r, g, b)), "rgb": [r, g, b], "ratio": round(count / total, 4)}
|
|
176
|
+
)
|
|
177
|
+
return out
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def analyze(path: str, bins: int, top: int) -> dict:
|
|
181
|
+
"""统计单张图片,返回机读 dict。读失败抛异常由调用方捕获。"""
|
|
182
|
+
file_size = os.path.getsize(path)
|
|
183
|
+
with Image.open(path) as img:
|
|
184
|
+
fmt = img.format
|
|
185
|
+
mode = img.mode
|
|
186
|
+
width, height = img.size
|
|
187
|
+
dpi = img.info.get("dpi")
|
|
188
|
+
has_alpha = (
|
|
189
|
+
mode in ("RGBA", "LA", "PA")
|
|
190
|
+
or (mode == "P" and "transparency" in img.info)
|
|
191
|
+
or "transparency" in img.info
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# 逐像素统计用的缩略 RGB 副本(thumbnail 只缩不放、保宽高比)。
|
|
195
|
+
rgb = img.convert("RGB")
|
|
196
|
+
|
|
197
|
+
analysis = rgb.copy()
|
|
198
|
+
analysis.thumbnail((ANALYSIS_MAX_SIDE, ANALYSIS_MAX_SIDE))
|
|
199
|
+
|
|
200
|
+
# 平均颜色 + 逐通道
|
|
201
|
+
rstat = ImageStat.Stat(analysis)
|
|
202
|
+
avg = rstat.mean # [r, g, b]
|
|
203
|
+
|
|
204
|
+
# 感知亮度(PIL 的 'L' 用 ITU-R 601-2:0.299R+0.587G+0.114B,正是感知亮度)
|
|
205
|
+
lum = analysis.convert("L")
|
|
206
|
+
lstat = ImageStat.Stat(lum)
|
|
207
|
+
lo, hi = lstat.extrema[0]
|
|
208
|
+
|
|
209
|
+
# 色调
|
|
210
|
+
tone = compute_tone(analysis.convert("HSV"))
|
|
211
|
+
is_gray = mode in ("L", "1", "I", "F") or tone["saturation"] < GRAYSCALE_SAT_MAX
|
|
212
|
+
category = classify_tone(tone["hue_deg"], is_gray)
|
|
213
|
+
|
|
214
|
+
# 直方图(分箱)
|
|
215
|
+
rgb_hist = analysis.histogram() # 768:R[0:256] G[256:512] B[512:768]
|
|
216
|
+
hist = {
|
|
217
|
+
"bins": bins if bins < 256 else 256,
|
|
218
|
+
"r": bin_histogram(rgb_hist[0:256], bins),
|
|
219
|
+
"g": bin_histogram(rgb_hist[256:512], bins),
|
|
220
|
+
"b": bin_histogram(rgb_hist[512:768], bins),
|
|
221
|
+
"luminance": bin_histogram(lum.histogram(), bins),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ar = width / height if height else 0.0
|
|
225
|
+
return {
|
|
226
|
+
"path": path,
|
|
227
|
+
"format": fmt,
|
|
228
|
+
"mode": mode,
|
|
229
|
+
"file_size_bytes": file_size,
|
|
230
|
+
"file_size_human": human_size(file_size),
|
|
231
|
+
"has_alpha": bool(has_alpha),
|
|
232
|
+
"width": width,
|
|
233
|
+
"height": height,
|
|
234
|
+
"aspect_ratio": round(ar, 4),
|
|
235
|
+
"orientation": classify_orientation(ar),
|
|
236
|
+
"megapixels": round(width * height / 1_000_000, 2),
|
|
237
|
+
"dpi": list(dpi) if dpi else None,
|
|
238
|
+
"average_color": {
|
|
239
|
+
"rgb": [round(c, 1) for c in avg],
|
|
240
|
+
"hex": to_hex(avg),
|
|
241
|
+
},
|
|
242
|
+
"brightness": {
|
|
243
|
+
"mean": round(lstat.mean[0], 1),
|
|
244
|
+
"stddev": round(lstat.stddev[0], 1),
|
|
245
|
+
"min": lo,
|
|
246
|
+
"max": hi,
|
|
247
|
+
},
|
|
248
|
+
"tone": {
|
|
249
|
+
"hue_deg": tone["hue_deg"],
|
|
250
|
+
"saturation": tone["saturation"],
|
|
251
|
+
"value": tone["value"],
|
|
252
|
+
"category": category,
|
|
253
|
+
"is_grayscale": bool(is_gray),
|
|
254
|
+
},
|
|
255
|
+
"dominant_colors": dominant_colors(analysis, top),
|
|
256
|
+
"histogram": hist,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _spark(counts) -> str:
|
|
261
|
+
"""把分箱直方图渲染成一行迷你火花线(仅文本报告用)。"""
|
|
262
|
+
blocks = "▁▂▃▄▅▆▇█"
|
|
263
|
+
peak = max(counts) if counts else 0
|
|
264
|
+
if peak == 0:
|
|
265
|
+
return " " * len(counts)
|
|
266
|
+
return "".join(blocks[min(7, int(c / peak * 7))] for c in counts)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def render_text(s: dict) -> str:
|
|
270
|
+
"""单张图片的人话报告。"""
|
|
271
|
+
L = []
|
|
272
|
+
L.append(f"📷 {s['path']}")
|
|
273
|
+
L.append(
|
|
274
|
+
f" 格式 {s['format']} / {s['mode']}"
|
|
275
|
+
f"{' / 含透明' if s['has_alpha'] else ''} 大小 {s['file_size_human']}"
|
|
276
|
+
)
|
|
277
|
+
dpi = f" DPI {s['dpi'][0]:g}×{s['dpi'][1]:g}" if s["dpi"] else ""
|
|
278
|
+
L.append(
|
|
279
|
+
f" 尺寸 {s['width']}×{s['height']}px"
|
|
280
|
+
f"({s['orientation']},比例 {s['aspect_ratio']},{s['megapixels']}MP){dpi}"
|
|
281
|
+
)
|
|
282
|
+
ac = s["average_color"]
|
|
283
|
+
L.append(f" 平均色 {ac['hex']} rgb{tuple(ac['rgb'])}")
|
|
284
|
+
b = s["brightness"]
|
|
285
|
+
L.append(
|
|
286
|
+
f" 亮度 均值 {b['mean']}/255 对比(σ) {b['stddev']} 范围 {b['min']}~{b['max']}"
|
|
287
|
+
)
|
|
288
|
+
t = s["tone"]
|
|
289
|
+
hue = "n/a" if t["hue_deg"] is None else f"{t['hue_deg']}°"
|
|
290
|
+
L.append(
|
|
291
|
+
f" 色调 {t['category']} hue {hue} 饱和 {t['saturation']} 明度 {t['value']}"
|
|
292
|
+
)
|
|
293
|
+
doms = " ".join(f"{d['hex']}({d['ratio'] * 100:.0f}%)" for d in s["dominant_colors"])
|
|
294
|
+
L.append(f" 主色 {doms}")
|
|
295
|
+
h = s["histogram"]
|
|
296
|
+
L.append(f" 直方图({h['bins']}bin) L {_spark(h['luminance'])}")
|
|
297
|
+
L.append(
|
|
298
|
+
f" R {_spark(h['r'])} G {_spark(h['g'])} B {_spark(h['b'])}"
|
|
299
|
+
)
|
|
300
|
+
return "\n".join(L)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def collect_targets(args) -> list:
|
|
304
|
+
"""把位置参数解析成图片路径列表(--scan 时把目录展开为受支持图片)。"""
|
|
305
|
+
targets = []
|
|
306
|
+
for t in args.targets:
|
|
307
|
+
if args.scan and os.path.isdir(t):
|
|
308
|
+
for name in sorted(os.listdir(t)):
|
|
309
|
+
if os.path.splitext(name)[1].lower() in SUPPORTED_EXT:
|
|
310
|
+
targets.append(os.path.join(t, name))
|
|
311
|
+
else:
|
|
312
|
+
targets.append(t)
|
|
313
|
+
return targets
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def main() -> int:
|
|
317
|
+
# 强制 UTF-8 输出:脚本含中文,Windows 默认 GBK 控制台/管道会乱码或崩溃,
|
|
318
|
+
# 宿主(minimal-agent Bash 工具已 chcp 65001)按 UTF-8 捕获才稳。
|
|
319
|
+
for stream in (sys.stdout, sys.stderr):
|
|
320
|
+
try:
|
|
321
|
+
stream.reconfigure(encoding="utf-8")
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
parser = argparse.ArgumentParser(
|
|
326
|
+
description="图像信息统计(确定性):宽高/分辨率/平均色/色调/主色/直方图"
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument("targets", nargs="+", help="图片路径(可多个);配 --scan 则为目录")
|
|
329
|
+
parser.add_argument("--scan", action="store_true", help="把目录参数展开为其中所有受支持图片(非递归)")
|
|
330
|
+
parser.add_argument("--json", action="store_true", help="输出机读 JSON 数组(stdout 只有 JSON)")
|
|
331
|
+
parser.add_argument("--bins", type=int, default=16, help="直方图分箱数(默认 16;256 = 全直方图)")
|
|
332
|
+
parser.add_argument("--top", type=int, default=5, help="主色个数(默认 5)")
|
|
333
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="把每张图进度打到 stderr")
|
|
334
|
+
args = parser.parse_args()
|
|
335
|
+
|
|
336
|
+
if args.bins < 1 or args.bins > 256:
|
|
337
|
+
print("参数错误:--bins 必须在 1~256 之间", file=sys.stderr)
|
|
338
|
+
return 2
|
|
339
|
+
if args.top < 1:
|
|
340
|
+
print("参数错误:--top 必须 >= 1", file=sys.stderr)
|
|
341
|
+
return 2
|
|
342
|
+
|
|
343
|
+
targets = collect_targets(args)
|
|
344
|
+
results = []
|
|
345
|
+
for path in targets:
|
|
346
|
+
if args.verbose:
|
|
347
|
+
print(f"[image-stats] 统计 {path}", file=sys.stderr)
|
|
348
|
+
try:
|
|
349
|
+
results.append(analyze(path, args.bins, args.top))
|
|
350
|
+
except Exception as e: # 单张失败不影响其余
|
|
351
|
+
print(f"[image-stats] 跳过 {path}:{e}", file=sys.stderr)
|
|
352
|
+
|
|
353
|
+
marker = f"IMAGE_STATS: 统计 {len(results)} 张图片(共 {len(targets)} 个目标)"
|
|
354
|
+
if args.json:
|
|
355
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
356
|
+
print(marker, file=sys.stderr) # JSON 模式 stdout 保持纯净,marker 走 stderr
|
|
357
|
+
else:
|
|
358
|
+
print("\n\n".join(render_text(s) for s in results) if results else "(无可统计图片)")
|
|
359
|
+
print("\n" + marker)
|
|
360
|
+
|
|
361
|
+
# 指定了目标却一张都没读出来 → 视为失败,让宿主/调用者能察觉。
|
|
362
|
+
if targets and not results:
|
|
363
|
+
return 1
|
|
364
|
+
return 0
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
sys.exit(main())
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
image_stats_test.py —— image_stats.py 的单测(PIL 合成图,自包含)。
|
|
5
|
+
|
|
6
|
+
锁定:
|
|
7
|
+
- 尺寸/朝向:1600×900 横图 → landscape + aspect>1.5;600×1000 竖图 → portrait + aspect<0.8。
|
|
8
|
+
- 平均色:纯红 → #FF0000、rgb≈(255,0,0)。
|
|
9
|
+
- 色调:纯红 → warm hue≈0;纯蓝 → cool hue≈240;灰图 → grayscale + hue=None。
|
|
10
|
+
- 亮度:纯白 → mean=255 / σ=0;半黑半白 → σ 高(对比大)。
|
|
11
|
+
- alpha:RGBA 图 → has_alpha=True。
|
|
12
|
+
- 主色:纯红 → 主色第一名 #FF0000 占比≈1。
|
|
13
|
+
- 直方图:bins=8 → 各通道 8 桶;bins=256 → 256 桶。
|
|
14
|
+
- CLI:--json 的 stdout 是纯 JSON 数组且 bin 数=指定值;读不出任何图 → 退出码 1。
|
|
15
|
+
依赖:Pillow(已就绪)。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import unittest
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
import image_stats as im # noqa: E402
|
|
27
|
+
|
|
28
|
+
from PIL import Image # noqa: E402
|
|
29
|
+
|
|
30
|
+
SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image_stats.py")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _png(path, w, h, color, mode="RGB"):
|
|
34
|
+
Image.new(mode, (w, h), color).save(path)
|
|
35
|
+
return path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestDimensions(unittest.TestCase):
|
|
39
|
+
def setUp(self):
|
|
40
|
+
self.tmp = tempfile.mkdtemp(prefix="imgstats-dim-")
|
|
41
|
+
|
|
42
|
+
def test_landscape(self):
|
|
43
|
+
p = _png(os.path.join(self.tmp, "l.png"), 1600, 900, (80, 120, 200))
|
|
44
|
+
s = im.analyze(p, bins=16, top=5)
|
|
45
|
+
self.assertEqual((s["width"], s["height"]), (1600, 900))
|
|
46
|
+
self.assertEqual(s["orientation"], "landscape")
|
|
47
|
+
self.assertGreater(s["aspect_ratio"], 1.5)
|
|
48
|
+
self.assertAlmostEqual(s["megapixels"], 1.44, places=2)
|
|
49
|
+
self.assertEqual(s["format"], "PNG")
|
|
50
|
+
|
|
51
|
+
def test_portrait(self):
|
|
52
|
+
p = _png(os.path.join(self.tmp, "p.png"), 600, 1000, (30, 30, 30))
|
|
53
|
+
s = im.analyze(p, bins=16, top=5)
|
|
54
|
+
self.assertEqual(s["orientation"], "portrait")
|
|
55
|
+
self.assertLess(s["aspect_ratio"], 0.8)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestColorAndTone(unittest.TestCase):
|
|
59
|
+
def setUp(self):
|
|
60
|
+
self.tmp = tempfile.mkdtemp(prefix="imgstats-color-")
|
|
61
|
+
|
|
62
|
+
def test_pure_red_warm(self):
|
|
63
|
+
p = _png(os.path.join(self.tmp, "red.png"), 64, 64, (255, 0, 0))
|
|
64
|
+
s = im.analyze(p, bins=16, top=5)
|
|
65
|
+
self.assertEqual(s["average_color"]["hex"], "#FF0000")
|
|
66
|
+
self.assertEqual([round(c) for c in s["average_color"]["rgb"]], [255, 0, 0])
|
|
67
|
+
self.assertEqual(s["tone"]["category"], "warm")
|
|
68
|
+
self.assertFalse(s["tone"]["is_grayscale"])
|
|
69
|
+
# hue 接近 0°(红)
|
|
70
|
+
self.assertTrue(s["tone"]["hue_deg"] < 15 or s["tone"]["hue_deg"] > 345)
|
|
71
|
+
|
|
72
|
+
def test_pure_blue_cool(self):
|
|
73
|
+
p = _png(os.path.join(self.tmp, "blue.png"), 64, 64, (0, 0, 255))
|
|
74
|
+
s = im.analyze(p, bins=16, top=5)
|
|
75
|
+
self.assertEqual(s["tone"]["category"], "cool")
|
|
76
|
+
self.assertAlmostEqual(s["tone"]["hue_deg"], 240, delta=15)
|
|
77
|
+
|
|
78
|
+
def test_gray_is_grayscale(self):
|
|
79
|
+
p = _png(os.path.join(self.tmp, "gray.png"), 64, 64, (128, 128, 128))
|
|
80
|
+
s = im.analyze(p, bins=16, top=5)
|
|
81
|
+
self.assertTrue(s["tone"]["is_grayscale"])
|
|
82
|
+
self.assertEqual(s["tone"]["category"], "grayscale")
|
|
83
|
+
self.assertIsNone(s["tone"]["hue_deg"])
|
|
84
|
+
|
|
85
|
+
def test_dominant_color(self):
|
|
86
|
+
p = _png(os.path.join(self.tmp, "red.png"), 64, 64, (255, 0, 0))
|
|
87
|
+
s = im.analyze(p, bins=16, top=5)
|
|
88
|
+
self.assertEqual(s["dominant_colors"][0]["hex"], "#FF0000")
|
|
89
|
+
self.assertAlmostEqual(s["dominant_colors"][0]["ratio"], 1.0, places=2)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestBrightnessAlpha(unittest.TestCase):
|
|
93
|
+
def setUp(self):
|
|
94
|
+
self.tmp = tempfile.mkdtemp(prefix="imgstats-bri-")
|
|
95
|
+
|
|
96
|
+
def test_white_brightness(self):
|
|
97
|
+
p = _png(os.path.join(self.tmp, "w.png"), 32, 32, (255, 255, 255))
|
|
98
|
+
s = im.analyze(p, bins=16, top=5)
|
|
99
|
+
self.assertEqual(s["brightness"]["mean"], 255)
|
|
100
|
+
self.assertEqual(s["brightness"]["stddev"], 0)
|
|
101
|
+
self.assertEqual((s["brightness"]["min"], s["brightness"]["max"]), (255, 255))
|
|
102
|
+
|
|
103
|
+
def test_high_contrast(self):
|
|
104
|
+
# 左半黑右半白 → 亮度标准差很大
|
|
105
|
+
img = Image.new("RGB", (64, 32), (0, 0, 0))
|
|
106
|
+
for x in range(32, 64):
|
|
107
|
+
for y in range(32):
|
|
108
|
+
img.putpixel((x, y), (255, 255, 255))
|
|
109
|
+
p = os.path.join(self.tmp, "split.png")
|
|
110
|
+
img.save(p)
|
|
111
|
+
s = im.analyze(p, bins=16, top=5)
|
|
112
|
+
self.assertGreater(s["brightness"]["stddev"], 100)
|
|
113
|
+
|
|
114
|
+
def test_rgba_has_alpha(self):
|
|
115
|
+
p = _png(os.path.join(self.tmp, "a.png"), 16, 16, (255, 0, 0, 128), mode="RGBA")
|
|
116
|
+
s = im.analyze(p, bins=16, top=5)
|
|
117
|
+
self.assertTrue(s["has_alpha"])
|
|
118
|
+
self.assertEqual(s["mode"], "RGBA")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestHistogram(unittest.TestCase):
|
|
122
|
+
def setUp(self):
|
|
123
|
+
self.tmp = tempfile.mkdtemp(prefix="imgstats-hist-")
|
|
124
|
+
|
|
125
|
+
def test_bins(self):
|
|
126
|
+
p = _png(os.path.join(self.tmp, "g.png"), 64, 64, (10, 200, 90))
|
|
127
|
+
s8 = im.analyze(p, bins=8, top=5)
|
|
128
|
+
self.assertEqual(s8["histogram"]["bins"], 8)
|
|
129
|
+
for ch in ("r", "g", "b", "luminance"):
|
|
130
|
+
self.assertEqual(len(s8["histogram"][ch]), 8)
|
|
131
|
+
s256 = im.analyze(p, bins=256, top=5)
|
|
132
|
+
self.assertEqual(s256["histogram"]["bins"], 256)
|
|
133
|
+
self.assertEqual(len(s256["histogram"]["r"]), 256)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestCli(unittest.TestCase):
|
|
137
|
+
def setUp(self):
|
|
138
|
+
self.tmp = tempfile.mkdtemp(prefix="imgstats-cli-")
|
|
139
|
+
|
|
140
|
+
def test_json_stdout_is_pure_json(self):
|
|
141
|
+
p = _png(os.path.join(self.tmp, "x.png"), 100, 50, (200, 50, 50))
|
|
142
|
+
r = subprocess.run(
|
|
143
|
+
[sys.executable, SCRIPT, p, "--json", "--bins", "8"],
|
|
144
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
145
|
+
)
|
|
146
|
+
self.assertEqual(r.returncode, 0)
|
|
147
|
+
data = json.loads(r.stdout) # stdout 必须是纯 JSON
|
|
148
|
+
self.assertEqual(len(data), 1)
|
|
149
|
+
self.assertEqual(data[0]["width"], 100)
|
|
150
|
+
self.assertEqual(len(data[0]["histogram"]["r"]), 8)
|
|
151
|
+
self.assertIn("IMAGE_STATS:", r.stderr) # marker 走 stderr 不污染 JSON
|
|
152
|
+
|
|
153
|
+
def test_text_mode_marker(self):
|
|
154
|
+
p = _png(os.path.join(self.tmp, "x.png"), 20, 20, (10, 10, 10))
|
|
155
|
+
r = subprocess.run([sys.executable, SCRIPT, p], capture_output=True, text=True, encoding="utf-8")
|
|
156
|
+
self.assertEqual(r.returncode, 0)
|
|
157
|
+
self.assertIn("IMAGE_STATS:", r.stdout)
|
|
158
|
+
|
|
159
|
+
def test_unreadable_target_exit_1(self):
|
|
160
|
+
bogus = os.path.join(self.tmp, "not-an-image.txt")
|
|
161
|
+
with open(bogus, "w") as f:
|
|
162
|
+
f.write("hello")
|
|
163
|
+
r = subprocess.run([sys.executable, SCRIPT, bogus], capture_output=True, text=True, encoding="utf-8")
|
|
164
|
+
self.assertEqual(r.returncode, 1) # 给了目标但一张都读不出来
|
|
165
|
+
|
|
166
|
+
def test_scan_dir(self):
|
|
167
|
+
_png(os.path.join(self.tmp, "a.png"), 40, 40, (1, 2, 3))
|
|
168
|
+
_png(os.path.join(self.tmp, "b.jpg"), 50, 30, (4, 5, 6))
|
|
169
|
+
with open(os.path.join(self.tmp, "note.txt"), "w") as f:
|
|
170
|
+
f.write("ignored")
|
|
171
|
+
r = subprocess.run(
|
|
172
|
+
[sys.executable, SCRIPT, self.tmp, "--scan", "--json"],
|
|
173
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
174
|
+
)
|
|
175
|
+
self.assertEqual(r.returncode, 0)
|
|
176
|
+
data = json.loads(r.stdout)
|
|
177
|
+
self.assertEqual(len(data), 2) # 只统计 png/jpg,忽略 txt
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
unittest.main(verbosity=2)
|