Nature Image2PPT Use this directory as the complete runtime. Run deterministic actions only through: Use Python 3.10 or later with installed. When a dedicated environment exists, substitute on macOS/Linux or on Windows for every command below. Do not continue after a failed ; install only the reported missing dependency, then rerun it. Do not discover or invoke another Skill, CLI, Prompt, Schema, module, or state machine. Read the local contracts progressively Always read . Read only for setup or failures, and read only when choosing or troubleshooting OCR. Before writing a page manifest, rea…

, pyproject).group(1)\n package_version = re.search(r'(?m)^__version__\\s*=\\s*\"([^\"]+)\"

Nature Image2PPT Use this directory as the complete runtime. Run deterministic actions only through: Use Python 3.10 or later with installed. When a dedicated environment exists, substitute on macOS/Linux or on Windows for every command below. Do not continue after a failed ; install only the reported missing dependency, then rerun it. Do not discover or invoke another Skill, CLI, Prompt, Schema, module, or state machine. Read the local contracts progressively Always read . Read only for setup or failures, and read only when choosing or troubleshooting OCR. Before writing a page manifest, rea…

, package).group(1)\n self.assertEqual(skill_version, project_version)\n self.assertEqual(skill_version, package_version)\n\n def test_requirements_match_package_dependencies(self) -> None:\n requirements = (SKILL_ROOT / \"requirements.txt\").read_text(encoding=\"utf-8\")\n pyproject = (SKILL_ROOT / \"cli\" / \"pyproject.toml\").read_text(encoding=\"utf-8\")\n\n def package_name(spec: str) -> str:\n return re.split(r\"[\u003c>=!~\\[]\", spec.strip(), maxsplit=1)[0].lower().replace(\"_\", \"-\")\n\n requirement_names = {\n package_name(line)\n for line in requirements.splitlines()\n if line.strip() and not line.lstrip().startswith(\"#\")\n }\n dependency_block = re.search(r\"(?s)dependencies\\s*=\\s*\\[(.*?)\\]\", pyproject).group(1)\n dependency_names = {\n package_name(spec) for spec in re.findall(r'\"([^\"]+)\"', dependency_block)\n }\n self.assertEqual(requirement_names, dependency_names)\n\n def test_readmes_are_mirrored_without_excluded_preview_assets(self) -> None:\n readme_cn = (SKILL_ROOT / \"README.md\").read_text(encoding=\"utf-8\")\n readme_en = (SKILL_ROOT / \"README_EN.md\").read_text(encoding=\"utf-8\")\n self.assertIn(\"[English](README_EN.md)\", readme_cn)\n self.assertIn(\"[中文说明](README.md)\", readme_en)\n for readme in (readme_cn, readme_en):\n self.assertIn(\"https://github.com/Paul-Jeo/Image2PPT\", readme)\n self.assertIn(\"config.example.yaml\", readme)\n self.assertIn(\"config.yaml\", readme)\n self.assertNotIn(\"assets/readme/\", readme)\n self.assertEqual(\n len(re.findall(r\"^## \", readme_cn, flags=re.MULTILINE)),\n len(re.findall(r\"^## \", readme_en, flags=re.MULTILINE)),\n )\n\n def test_public_config_template_contains_no_secret(self) -> None:\n yaml_template = (SKILL_ROOT / \"config.example.yaml\").read_text(encoding=\"utf-8\")\n self.assertRegex(yaml_template, r'(?m)^PADDLE_OCR_TOKEN:\\s*\"\"\\s*

Nature Image2PPT Use this directory as the complete runtime. Run deterministic actions only through: Use Python 3.10 or later with installed. When a dedicated environment exists, substitute on macOS/Linux or on Windows for every command below. Do not continue after a failed ; install only the reported missing dependency, then rerun it. Do not discover or invoke another Skill, CLI, Prompt, Schema, module, or state machine. Read the local contracts progressively Always read . Read only for setup or failures, and read only when choosing or troubleshooting OCR. Before writing a page manifest, rea…

)\n self.assertNotIn(\"sk-\", yaml_template)\n self.assertFalse((SKILL_ROOT / \".env.example\").exists())\n\n def test_secret_files_and_generated_runs_are_ignored(self) -> None:\n ignore = (SKILL_ROOT / \".gitignore\").read_text(encoding=\"utf-8\")\n patterns = (\".env\", \"config.yaml\", \"output/\", \"runs/\", \"__pycache__/\", \"assets/readme/\", \".github/\")\n for pattern in patterns:\n self.assertIn(pattern, ignore)\n\n def test_openai_metadata_has_required_user_facing_fields(self) -> None:\n metadata = (SKILL_ROOT / \"agents\" / \"openai.yaml\").read_text(encoding=\"utf-8\")\n self.assertIn('display_name: \"Nature Image2PPT\"', metadata)\n self.assertIn(\"$nature-image2ppt\", metadata)\n match = re.search(r'^\\s*short_description:\\s*\"([^\"]+)\"', metadata, flags=re.MULTILINE)\n self.assertIsNotNone(match)\n self.assertGreaterEqual(len(match.group(1)), 25)\n self.assertLessEqual(len(match.group(1)), 64)\n\n def test_page_manifest_v2_json_schema_is_valid_json(self) -> None:\n schema = (SKILL_ROOT / \"schemas\" / \"page-manifest-v2.schema.json\")\n payload = json.loads(schema.read_text(encoding=\"utf-8\"))\n self.assertEqual(payload[\"properties\"][\"schema_version\"][\"const\"], 2)\n self.assertIn(\"quality_evidence\", payload[\"required\"])\n\n def test_markdown_references_current_skill_sections(self) -> None:\n markdown = \"\\n\".join(\n path.read_text(encoding=\"utf-8\")\n for root in (SKILL_ROOT / \"references\", SKILL_ROOT / \"prompts\")\n for path in sorted(root.glob(\"*.md\"))\n )\n for stale_reference in (\n \"SKILL.md Entry Contract\",\n \"SKILL.md Phase 1\",\n \"SKILL.md Phase 3\",\n \"SKILL.md Phase 4\",\n 'SKILL.md subsection \"Image Backend Selection\"',\n \"State Principles section of `SKILL.md`\",\n ):\n self.assertNotIn(stale_reference, markdown)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4778,"content_sha256":"ba7d893d84c0724ab1b02e4463b6a883ea6bbf48d5f40dbdbe488340f76641d9"},{"filename":"tests/test_quality_contracts.py","content":"import sys\nimport unittest\nfrom pathlib import Path\n\n\nROOT = Path(__file__).resolve().parents[1]\nRUNTIME_DIR = ROOT / \"cli/image2ppt/runtime\"\nsys.path.insert(0, str(RUNTIME_DIR))\n\nfrom validate_pptx import ALLOWED_SOURCE_TYPES, quality_contract_violations, required_texts_from_manifest # noqa: E402\nfrom build_pptx_from_manifest import shape_xml # noqa: E402\n\n\ndef base_manifest():\n return {\n \"visual_inventory\": [],\n \"background_strategy\": {\n \"mode\": \"native-or-script\",\n \"comparison_note\": \"background checked against source\",\n },\n \"quality_checks\": {\n \"font_size_calibrated\": True,\n \"visual_inventory_matched\": True,\n \"background_strategy_checked\": True,\n \"shape_corner_geometry_checked\": True,\n },\n \"shapes\": [],\n }\n\n\ndef v2_manifest():\n manifest = base_manifest()\n manifest[\"schema_version\"] = 2\n manifest[\"quality_evidence\"] = {\n key: {\"observation\": f\"Verified {key} against the source image and current render.\"}\n for key in manifest[\"quality_checks\"]\n }\n return manifest\n\n\nclass QualityContractTest(unittest.TestCase):\n def test_quality_checks_are_required(self):\n violations = quality_contract_violations({})\n fields = {item[\"field\"] for item in violations}\n self.assertIn(\"visual_inventory\", fields)\n self.assertIn(\"background_strategy\", fields)\n self.assertIn(\"quality_checks\", fields)\n\n def test_round_rect_requires_source_evidence(self):\n manifest = base_manifest()\n manifest[\"shapes\"] = [{\"type\": \"roundRect\", \"box_px\": [0, 0, 100, 40]}]\n violations = quality_contract_violations(manifest)\n self.assertEqual([\"shapes[0]\"], [item[\"field\"] for item in violations])\n\n def test_rect_does_not_need_corner_evidence(self):\n manifest = base_manifest()\n manifest[\"shapes\"] = [{\"type\": \"rect\", \"box_px\": [0, 0, 100, 40]}]\n self.assertEqual([], quality_contract_violations(manifest))\n\n def test_source_derived_assets_are_not_allowed(self):\n self.assertNotIn(\"source-derived-rasterization\", ALLOWED_SOURCE_TYPES)\n\n def test_latex_rendered_formula_assets_are_allowed(self):\n self.assertIn(\"latex-rendered-formula\", ALLOWED_SOURCE_TYPES)\n\n def test_asset_sheet_separated_assets_are_allowed(self):\n self.assertIn(\"asset-sheet-separated\", ALLOWED_SOURCE_TYPES)\n\n def test_foreground_native_approximation_is_contract_violation(self):\n manifest = base_manifest()\n manifest[\"visual_inventory\"] = [\n {\n \"id\": \"bottom_icon\",\n \"description\": \"semantic icon in the bottom flow\",\n \"decision\": \"native approximation with text symbol\",\n }\n ]\n violations = quality_contract_violations(manifest)\n reasons = \" \".join(item[\"reason\"] for item in violations)\n self.assertIn(\"foreground visual decisions\", reasons)\n\n def test_foreground_direct_crop_provenance_is_contract_violation(self):\n manifest = base_manifest()\n manifest[\"visual_inventory\"] = [\n {\n \"id\": \"photo_panel\",\n \"description\": \"foreground photo panel\",\n \"decision\": \"source-faithful asset-sheet separation\",\n \"path\": \"assets/source_crops/photo.png\",\n }\n ]\n manifest[\"asset_provenance\"] = [\n {\n \"path\": \"assets/source_crops/photo.png\",\n \"source_type\": \"user-provided\",\n \"source\": \"source.png\",\n \"provenance_note\": \"cropped from source foreground photo\",\n }\n ]\n violations = quality_contract_violations(manifest)\n fields = [item[\"field\"] for item in violations]\n self.assertIn(\"visual_inventory[0]\", fields)\n self.assertIn(\"asset_provenance[0]\", fields)\n\n def test_foreground_asset_sheet_decision_passes_contract(self):\n manifest = base_manifest()\n manifest[\"visual_inventory\"] = [\n {\n \"id\": \"photo_panel\",\n \"description\": \"foreground photo panel\",\n \"decision\": \"source-faithful asset-sheet separation through image2ppt image edit\",\n \"path\": \"assets/photo_panel.png\",\n }\n ]\n manifest[\"asset_provenance\"] = [\n {\n \"path\": \"assets/photo_panel.png\",\n \"source_type\": \"asset-sheet-separated\",\n \"source\": \"assets/photo_sheet.png\",\n \"provenance_note\": \"split from source-faithful asset sheet generated with image2ppt image edit\",\n }\n ]\n self.assertEqual([], quality_contract_violations(manifest))\n\n def test_structured_native_item_avoids_benchmark_substring_false_positive(self):\n manifest = v2_manifest()\n manifest[\"visual_inventory\"] = [\n {\n \"id\": \"benchmark_panel\",\n \"kind\": \"native-structure\",\n \"representation\": \"native\",\n \"description\": \"benchmark result panel\",\n }\n ]\n self.assertEqual([], quality_contract_violations(manifest))\n\n def test_structured_foreground_item_cannot_hide_behind_background_word(self):\n manifest = v2_manifest()\n manifest[\"visual_inventory\"] = [\n {\n \"id\": \"icon\",\n \"kind\": \"foreground-asset\",\n \"representation\": \"native\",\n \"description\": \"foreground icon over background\",\n }\n ]\n reasons = \" \".join(item[\"reason\"] for item in quality_contract_violations(manifest))\n self.assertIn(\"foreground-asset requires\", reasons)\n\n def test_unrendered_formula_is_a_hard_failure_without_user_approval(self):\n manifest = v2_manifest()\n manifest[\"formula_inventory\"] = [\n {\"id\": \"f1\", \"status\": \"failed\", \"tex_source\": \"assets/f1.tex\"}\n ]\n reasons = \" \".join(item[\"reason\"] for item in quality_contract_violations(manifest))\n self.assertIn(\"hard failure\", reasons)\n\n def test_formula_visual_requires_formula_inventory(self):\n manifest = v2_manifest()\n manifest[\"visual_inventory\"] = [\n {\n \"id\": \"f1\",\n \"kind\": \"formula\",\n \"representation\": \"latex-rendered-formula\",\n \"path\": \"assets/f1.svg\",\n }\n ]\n reasons = \" \".join(item[\"reason\"] for item in quality_contract_violations(manifest))\n self.assertIn(\"matching formula_inventory\", reasons)\n\n def test_user_approved_formula_exception_requires_and_accepts_concrete_note(self):\n manifest = v2_manifest()\n manifest[\"formula_inventory\"] = [\n {\n \"id\": \"f1\",\n \"status\": \"failed\",\n \"tex_source\": \"assets/f1.tex\",\n \"user_approved_exception\": True,\n \"approval_note\": \"User explicitly approved delivery without this secondary formula.\",\n }\n ]\n self.assertEqual([], quality_contract_violations(manifest))\n\n def test_v2_quality_booleans_require_specific_evidence(self):\n manifest = v2_manifest()\n manifest[\"quality_evidence\"][\"visual_inventory_matched\"] = {\"observation\": \"checked\"}\n fields = [item[\"field\"] for item in quality_contract_violations(manifest)]\n self.assertIn(\"quality_evidence.visual_inventory_matched.observation\", fields)\n\n def test_round_rect_writes_ooxml_adjustment(self):\n xml = shape_xml(\n 2,\n {\n \"type\": \"roundRect\",\n \"box_px\": [0, 0, 400, 200],\n \"left\": 0,\n \"top\": 0,\n \"width\": 4,\n \"height\": 2,\n \"source_corner_radius_px\": 10,\n \"fill\": \"none\",\n \"stroke\": \"#000000\",\n },\n )\n self.assertIn('prst=\"roundRect\"', xml)\n self.assertIn('name=\"adj\"', xml)\n self.assertIn('fmla=\"val 5000\"', xml)\n\n def test_structured_text_inventory_flattens_to_required_strings(self):\n required = required_texts_from_manifest(\n {\n \"required_text\": [\"市场概览\"],\n \"text_inventory\": [\n {\"id\": \"metric\", \"text\": \"4280 万\", \"decision\": \"native-text\"},\n {\"id\": \"insights\", \"required_text\": [\"扩张\", \"续约\"]},\n {\"id\": \"note\", \"description\": \"not an exact text requirement\"},\n ],\n }\n )\n self.assertEqual([\"市场概览\", \"4280 万\", \"扩张\", \"续约\"], required)\n\n def test_invalid_text_alignment_is_a_contract_violation(self):\n manifest = base_manifest()\n manifest[\"text_boxes\"] = [\n {\n \"text\": \"1\",\n \"box_px\": [0, 0, 40, 40],\n \"align\": \"sideways\",\n \"valign\": \"floating\",\n }\n ]\n\n violations = quality_contract_violations(manifest)\n fields = {item[\"field\"] for item in violations}\n\n self.assertIn(\"text_boxes[0].align\", fields)\n self.assertIn(\"text_boxes[0].valign\", fields)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9279,"content_sha256":"70f80061d58dca3dc9d0dd7632c9c6e6c4b27047ca1f2f8a0f672754bf7ade70"},{"filename":"tests/test_region_decomposition.py","content":"from __future__ import annotations\n\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nfrom support import write_json\n\nfrom inspect_region_decomposition import read_json, validate_manifest\n\n\ndef knowledge_graph_manifest(page: Path, *, distorted: bool = False, flattened: bool = False) -> Path:\n nodes = [\n (\"node_concept\", [100, 80, 24, 34] if distorted else [100, 85, 24, 24], [112, 97], [24, 24]),\n (\"node_threshold\", [100, 145, 24, 24], [112, 157], [24, 24]),\n (\"node_exception\", [100, 205, 24, 24], [112, 217], [24, 24]),\n ]\n shapes = [\n {\"id\": item_id, \"type\": \"ellipse\", \"box_px\": box}\n for item_id, box, _center, _size in nodes\n ]\n shapes.extend(\n [\n {\n \"id\": \"edge_concept_threshold\",\n \"type\": \"line\",\n \"points_px\": [112, 109, 112, 145],\n \"end_arrow\": \"triangle\",\n },\n {\n \"id\": \"edge_threshold_exception\",\n \"type\": \"line\",\n \"points_px\": [112, 169, 112, 205],\n \"dash\": \"dash\",\n \"end_arrow\": \"triangle\",\n },\n ]\n )\n manifest_ids = [item[\"id\"] for item in shapes]\n anchors = [\n {\n \"id\": f\"anchor-{item_id}\",\n \"kind\": \"circle-node\",\n \"manifest_id\": item_id,\n \"source_bbox_px\": [center[0] - 12, center[1] - 12, 24, 24],\n }\n for item_id, _box, center, _size in nodes\n ]\n anchors.extend(\n [\n {\n \"id\": \"anchor-edge-concept-threshold\",\n \"kind\": \"directed-connector\",\n \"manifest_id\": \"edge_concept_threshold\",\n \"source_points_px\": [112, 109, 112, 145],\n },\n {\n \"id\": \"anchor-edge-threshold-exception\",\n \"kind\": \"dashed-relationship\",\n \"manifest_id\": \"edge_threshold_exception\",\n \"source_points_px\": [112, 169, 112, 205],\n },\n ]\n )\n compound = {\n \"kind\": \"knowledge-graph\",\n \"object_inventory_complete\": True,\n \"measurement_reviewed\": True,\n \"native_object_policy\": \"measured-simple-objects-native\",\n \"nodes\": [\n {\n \"id\": item_id.removeprefix(\"node_\"),\n \"manifest_id\": item_id,\n \"geometry\": \"circle\",\n \"source_center_px\": center,\n \"source_size_px\": size,\n \"position_tolerance_px\": 2,\n \"size_tolerance_px\": 2,\n }\n for item_id, _box, center, size in nodes\n ],\n \"edges\": [\n {\n \"id\": \"concept-threshold\",\n \"manifest_id\": \"edge_concept_threshold\",\n \"source_points_px\": [112, 109, 112, 145],\n \"line_style\": \"solid\",\n \"direction\": \"end\",\n },\n {\n \"id\": \"threshold-exception\",\n \"manifest_id\": \"edge_threshold_exception\",\n \"source_points_px\": [112, 169, 112, 205],\n \"line_style\": \"dashed\",\n \"direction\": \"end\",\n },\n ],\n }\n region = {\n \"id\": \"knowledge-graph\",\n \"label\": \"Supplementary Knowledge Graph Context\",\n \"source_bbox_px\": [50, 40, 230, 240],\n \"risk_level\": \"high\",\n \"strategy\": \"bounded-local-asset\" if flattened else \"native-object-decomposition\",\n \"reason\": \"Measured graph nodes and relationships.\",\n \"manifest_ids\": {\"shapes\": manifest_ids, \"text_boxes\": [], \"images\": []},\n \"protected_visual_anchors\": anchors,\n \"compound_diagram\": compound,\n }\n empty_regions = [\n {\n \"id\": \"title\",\n \"label\": \"Title\",\n \"source_bbox_px\": [0, 0, 640, 40],\n \"risk_level\": \"low\",\n \"strategy\": \"native-text-layout\",\n \"reason\": \"Simple title region.\",\n \"manifest_ids\": {\"shapes\": [], \"text_boxes\": [\"title_text\"], \"images\": []},\n \"protected_visual_anchors\": [],\n },\n {\n \"id\": \"legend\",\n \"label\": \"Legend\",\n \"source_bbox_px\": [300, 40, 300, 100],\n \"risk_level\": \"low\",\n \"strategy\": \"native-text-layout\",\n \"reason\": \"Simple legend region.\",\n \"manifest_ids\": {\"shapes\": [], \"text_boxes\": [\"legend_text\"], \"images\": []},\n \"protected_visual_anchors\": [],\n },\n ]\n manifest = {\n \"source\": {\"path\": \"source.png\", \"width_px\": 640, \"height_px\": 360},\n \"shapes\": shapes,\n \"text_boxes\": [{\"id\": \"title_text\"}, {\"id\": \"legend_text\"}],\n \"images\": [],\n \"image2ppt_region_decomposition\": {\n \"schema_version\": \"image2ppt-region-decomposition-v1\",\n \"page_complexity\": \"structured\",\n \"source_size_px\": [640, 360],\n \"regions\": [*empty_regions, region],\n },\n }\n path = page / \"manifest.json\"\n write_json(path, manifest)\n return path\n\n\nclass RegionDecompositionTests(unittest.TestCase):\n def test_measured_knowledge_graph_nodes_and_edges_pass(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n path = knowledge_graph_manifest(Path(name))\n report = validate_manifest(read_json(path), path)\n self.assertTrue(report[\"passed\"], report)\n self.assertEqual(report[\"summary\"][\"compound_regions\"], 1)\n\n def test_distorted_circle_is_rejected(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n path = knowledge_graph_manifest(Path(name), distorted=True)\n report = validate_manifest(read_json(path), path)\n self.assertFalse(report[\"passed\"])\n self.assertTrue(any(\"distorted ellipse\" in item or \"source size\" in item for item in report[\"errors\"]))\n\n def test_whole_compound_diagram_bitmap_strategy_is_rejected(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n path = knowledge_graph_manifest(Path(name), flattened=True)\n report = validate_manifest(read_json(path), path)\n self.assertFalse(report[\"passed\"])\n self.assertTrue(any(\"cannot be replaced\" in item for item in report[\"errors\"]))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6357,"content_sha256":"2984d35ac93b56cc5cce81ec044328ab1160d8aec0312abc2f8f695ce4b6fe6f"},{"filename":"tests/test_render_guard.py","content":"from __future__ import annotations\n\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nfrom PIL import Image\n\nfrom support import SCRIPTS\nfrom render_image2ppt_qa import render_powerpoint\n\n\nclass RenderGuardTests(unittest.TestCase):\n def test_powerpoint_command_releases_deck_before_application(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n root = Path(name)\n completed = subprocess.CompletedProcess([], 1, \"\", \"fixture\")\n with (\n patch(\"render_image2ppt_qa.shutil.which\", return_value=\"powershell.exe\"),\n patch(\"render_image2ppt_qa.subprocess.run\", return_value=completed),\n ):\n _rendered, raw = render_powerpoint(root / \"page.pptx\", root / \"render\", 1)\n\n script = raw[\"command\"][-1]\n self.assertLess(script.index(\"$app=$null;$deck=$null;\"), script.index(\"try{\"))\n self.assertIn(\"finally{if($null -ne $deck)\", script)\n self.assertLess(script.index(\"$deck.Close()\"), script.index(\"ReleaseComObject($deck)\"))\n self.assertLess(script.index(\"ReleaseComObject($deck)\"), script.index(\"$app.Quit()\"))\n self.assertLess(script.index(\"$app.Quit()\"), script.index(\"ReleaseComObject($app)\"))\n\n def test_source_image_cannot_be_reused_as_render_proof(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n root = Path(name)\n source = root / \"source.png\"\n Image.new(\"RGB\", (320, 180), \"white\").save(source)\n pptx = root / \"page.pptx\"\n pptx.write_bytes(b\"not-needed-before-source-reuse-guard\")\n report = root / \"render_report.json\"\n result = subprocess.run(\n [\n sys.executable,\n str(SCRIPTS / \"render_image2ppt_qa.py\"),\n str(pptx),\n \"--out-dir\",\n str(root / \"render\"),\n \"--report\",\n str(report),\n \"--source\",\n str(source),\n \"--existing-rendered\",\n str(source),\n ],\n text=True,\n capture_output=True,\n )\n self.assertNotEqual(result.returncode, 0)\n self.assertIn(\"source reuse\", report.read_text(encoding=\"utf-8\"))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2489,"content_sha256":"3780ef17a2b73f1d17a291fd4c77ce8d76bee4489112f351c4ad649b1e0f88fc"},{"filename":"tests/test_runtime_config.py","content":"from __future__ import annotations\n\nimport os\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\nfrom unittest import mock\n\nimport yaml\n\n\nROOT = Path(__file__).resolve().parents[1]\nCLI_DIR = ROOT / \"cli\"\nsys.path.insert(0, str(CLI_DIR))\n\nfrom image2ppt.runtime import runtime_env # noqa: E402\n\n\nclass RuntimeConfigTests(unittest.TestCase):\n def test_update_restricts_existing_config_permissions(self) -> None:\n with tempfile.TemporaryDirectory() as directory:\n config = Path(directory) / \"config.yaml\"\n config.write_text(\"OPENAI_API_KEY: old-key\\n\", encoding=\"utf-8\")\n if sys.platform != \"win32\":\n os.chmod(config, 0o644)\n\n runtime_env.write_config_file(config, {\"OPENAI_API_KEY\": \"new-key\"})\n\n self.assertEqual(\n runtime_env.read_config_file(config),\n {\"OPENAI_API_KEY\": \"new-key\"},\n )\n if sys.platform != \"win32\":\n self.assertEqual(config.stat().st_mode & 0o777, 0o600)\n\n def test_failed_serialization_preserves_existing_config(self) -> None:\n with tempfile.TemporaryDirectory() as directory:\n config = Path(directory) / \"config.yaml\"\n original = \"OPENAI_API_KEY: existing-key\\n\"\n config.write_text(original, encoding=\"utf-8\")\n\n with mock.patch.object(yaml, \"safe_dump\", side_effect=RuntimeError(\"write failed\")):\n with self.assertRaisesRegex(RuntimeError, \"write failed\"):\n runtime_env.write_config_file(config, {\"OPENAI_API_KEY\": \"replacement-key\"})\n\n self.assertEqual(config.read_text(encoding=\"utf-8\"), original)\n self.assertEqual(list(config.parent.glob(f\".{config.name}.*\")), [])\n\n def test_failed_flush_or_replace_preserves_existing_config(self) -> None:\n for operation in (\"fsync\", \"replace\"):\n with self.subTest(operation=operation), tempfile.TemporaryDirectory() as directory:\n config = Path(directory) / \"config.yaml\"\n original = b\"OPENAI_API_KEY: existing-key\\n\"\n config.write_bytes(original)\n\n with mock.patch.object(\n runtime_env.os, operation, side_effect=OSError(\"disk error\")\n ):\n with self.assertRaisesRegex(OSError, \"disk error\"):\n runtime_env.write_config_file(config, {\"OPENAI_API_KEY\": \"new-key\"})\n\n self.assertEqual(config.read_bytes(), original)\n self.assertEqual(list(config.parent.glob(f\".{config.name}.*\")), [])\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2652,"content_sha256":"3860eb7ebd6e2bcc12fe89aa7d5185bdbf46f176d79f81318830e39ae7b2b17c"},{"filename":"tests/test_runtime_paths.py","content":"from __future__ import annotations\n\nimport json\nimport os\nimport subprocess\nimport sys\nimport unittest\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nfrom support import SCRIPTS, SKILL_ROOT\n\nsys.path.insert(0, str(SCRIPTS))\nfrom runtime_paths import ( # noqa: E402\n CLI_ENTRY,\n RUNTIME_ROOT,\n image2ppt_command,\n image2ppt_runtime_script,\n required_runtime_files,\n runtime_path_report,\n)\n\n\nclass RuntimePathTests(unittest.TestCase):\n def test_every_declared_local_resource_exists(self) -> None:\n missing = {name: path for name, path in required_runtime_files().items() if not path.is_file()}\n self.assertEqual(missing, {})\n\n def test_runtime_probe_uses_only_local_owners(self) -> None:\n report = runtime_path_report()\n self.assertEqual(Path(report[\"cli_entry\"]), CLI_ENTRY)\n self.assertEqual(Path(report[\"runtime_root\"]), RUNTIME_ROOT)\n self.assertTrue(all(item[\"exists\"] for item in report[\"files\"].values()))\n\n def test_runtime_probe_parses_complete_doctor_json(self) -> None:\n result = subprocess.run(\n [*image2ppt_command(), \"doctor\", \"--json\"],\n text=True,\n capture_output=True,\n check=False,\n )\n self.assertEqual(result.returncode, 0, result.stdout + result.stderr)\n report = json.loads(result.stdout)\n self.assertTrue(report[\"ok\"])\n self.assertIn(report[\"ocr\"][\"selection\"], {\"paddleocr-vl\", \"builtin-ink\"})\n\n def test_command_is_fixed_to_source_tree_cli(self) -> None:\n poisoned = {\n \"IMAGE2PPT_COMMAND\": \"/tmp/not-used\",\n \"IMAGE2PPT_SKILL_ROOT\": \"/tmp/not-used\",\n \"PYTHONPATH\": \"/tmp/not-used\",\n }\n with patch.dict(os.environ, poisoned, clear=False):\n command = image2ppt_command()\n self.assertEqual(command, [sys.executable, str(SKILL_ROOT / \"cli\" / \"image2ppt\" / \"cli.py\")])\n\n def test_runtime_path_cannot_escape_local_runtime(self) -> None:\n with self.assertRaises(ValueError):\n image2ppt_runtime_script(\"../../outside.py\")\n self.assertEqual(image2ppt_runtime_script(\"main.py\").parent, RUNTIME_ROOT)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2231,"content_sha256":"65c4fab4707bd9c19ff2dbf4eae933f16a4030d3fe430949f59d061a5860f30f"},{"filename":"tests/test_script_inventory.py","content":"from __future__ import annotations\n\nimport unittest\nfrom pathlib import Path\n\n\nROOT = Path(__file__).resolve().parents[1]\nRUNTIME_DIR = ROOT / \"cli\" / \"image2ppt\" / \"runtime\"\nSCRIPT_DIR = ROOT / \"scripts\"\n\n\nclass ScriptInventoryTests(unittest.TestCase):\n def test_required_local_entrypoints_exist(self) -> None:\n required = {\n ROOT / \"cli/image2ppt/cli.py\",\n SCRIPT_DIR / \"build_page_worker_prompt.py\",\n SCRIPT_DIR / \"run_image2ppt_qa.py\",\n SCRIPT_DIR / \"run_final_image2ppt_qa.py\",\n SCRIPT_DIR / \"visual_review_evidence.py\",\n SCRIPT_DIR / \"inspect_region_decomposition.py\",\n SCRIPT_DIR / \"inspect_arrow_atomicity.py\",\n RUNTIME_DIR / \"paddle_text_hints.py\",\n RUNTIME_DIR / \"deck_text_hints.py\",\n RUNTIME_DIR / \"build_pptx_from_manifest.py\",\n RUNTIME_DIR / \"validate_pptx.py\",\n RUNTIME_DIR / \"finalize_deck_run.py\",\n ROOT / \"schemas/page-manifest-v2.schema.json\",\n }\n self.assertEqual([str(path) for path in sorted(required) if not path.is_file()], [])\n\n def test_doctor_is_the_only_runtime_preflight(self) -> None:\n self.assertFalse((SCRIPT_DIR / \"check_runtime.py\").exists())\n self.assertTrue((ROOT / \"cli/image2ppt/runtime/runtime_env.py\").is_file())\n\n def test_deprecated_parallel_entrypoints_are_absent(self) -> None:\n names = {\n \"prepare_inputs.py\",\n \"run_page_experiment.py\",\n \"normalize_paddle_ocr.py\",\n \"assemble_image2ppt_plans.py\",\n \"package_image2ppt.py\",\n }\n present = [path.name for root in (RUNTIME_DIR, SCRIPT_DIR) for path in root.glob(\"*.py\") if path.name in names]\n self.assertEqual(present, [])\n\n def test_no_symlink_escapes_the_skill(self) -> None:\n escaped = []\n for path in ROOT.rglob(\"*\"):\n if path.is_symlink():\n try:\n path.resolve().relative_to(ROOT.resolve())\n except ValueError:\n escaped.append(str(path))\n self.assertEqual(escaped, [])\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2186,"content_sha256":"af26f192eaead9a36377084b53003535cfc37207009edd3cfa0b1ce6a44e7b11"},{"filename":"tests/test_slide_layout.py","content":"import sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nfrom PIL import Image\n\n\nROOT = Path(__file__).resolve().parents[1]\nRUNTIME_DIR = ROOT / \"cli/image2ppt/runtime\"\nsys.path.insert(0, str(RUNTIME_DIR))\n\nfrom build_pptx_from_manifest import ( # noqa: E402\n content_box_for_manifest,\n emu,\n normalize_manifest,\n px_to_inches,\n render_preview,\n slide_size_type,\n text_box_xml,\n)\nfrom prepare_deck_run import fit_content_box, slide_for_source # noqa: E402\n\n\ndef scalable_test_font():\n candidates = (\n \"C:/Windows/Fonts/arial.ttf\",\n \"C:/Windows/Fonts/calibri.ttf\",\n \"C:/Windows/Fonts/msyh.ttc\",\n \"/System/Library/Fonts/Supplemental/Arial.ttf\",\n \"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\",\n \"/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf\",\n )\n return next((path for path in candidates if Path(path).exists()), None)\n\n\ndef preview_ink_center(manifest):\n with tempfile.TemporaryDirectory() as tmp:\n preview_path = Path(tmp) / \"preview.png\"\n render_preview(manifest, Path(tmp) / \"manifest.json\", preview_path)\n image = Image.open(preview_path).convert(\"RGB\")\n\n dark_pixels = [\n (x, y)\n for y in range(image.height)\n for x in range(image.width)\n if max(image.getpixel((x, y))) \u003c 128\n ]\n if not dark_pixels:\n return None\n xs = [point[0] for point in dark_pixels]\n ys = [point[1] for point in dark_pixels]\n return (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2\n\n\nclass SlideLayoutTest(unittest.TestCase):\n def test_non_wide_source_uses_source_pixel_size(self):\n slide = slide_for_source(1536, 1024)\n\n self.assertEqual(\"source\", slide[\"size_mode\"])\n self.assertAlmostEqual(16, slide[\"width\"])\n self.assertAlmostEqual(10.6666667, slide[\"height\"])\n\n box = fit_content_box(1536, 1024, slide)\n self.assertAlmostEqual(0, box[\"left\"])\n self.assertAlmostEqual(0, box[\"top\"])\n self.assertAlmostEqual(slide[\"width\"], box[\"width\"])\n self.assertAlmostEqual(slide[\"height\"], box[\"height\"])\n\n def test_near_wide_source_snaps_to_standard_wide_without_stretching(self):\n slide = slide_for_source(1918, 1080)\n\n self.assertEqual(\"wide\", slide[\"size_mode\"])\n self.assertAlmostEqual(13.333, slide[\"width\"])\n self.assertAlmostEqual(7.5, slide[\"height\"])\n\n box = fit_content_box(1918, 1080, slide)\n self.assertAlmostEqual(7.5, box[\"height\"])\n self.assertGreater(box[\"left\"], 0)\n self.assertLess(box[\"width\"], slide[\"width\"])\n\n def test_pixel_coordinates_map_to_content_box(self):\n manifest = {\n \"source\": {\"width_px\": 1536, \"height_px\": 1024},\n \"slide\": {\"width\": 13.333, \"height\": 7.5},\n }\n\n box = content_box_for_manifest(manifest)\n self.assertAlmostEqual(1.0415, box[\"left\"], places=3)\n self.assertAlmostEqual(0, box[\"top\"])\n self.assertAlmostEqual(11.25, box[\"width\"])\n self.assertAlmostEqual(7.5, box[\"height\"])\n\n position = px_to_inches(manifest, 0, 0, 1536, 1024)\n self.assertAlmostEqual(box[\"left\"], position[\"left\"])\n self.assertAlmostEqual(box[\"top\"], position[\"top\"])\n self.assertAlmostEqual(box[\"width\"], position[\"width\"])\n self.assertAlmostEqual(box[\"height\"], position[\"height\"])\n\n def test_presentation_size_metadata_uses_valid_ooxml_values(self):\n self.assertEqual(\"custom\", slide_size_type(emu(16), emu(10.6666667)))\n self.assertEqual(\"screen16x9\", slide_size_type(emu(13.333), emu(7.5)))\n\n def test_text_font_size_is_clamped_to_source_box(self):\n manifest = {\n \"source\": {\"width_px\": 1600, \"height_px\": 900},\n \"slide\": {\"width\": 13.333, \"height\": 7.5},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 13.333, \"height\": 7.5},\n \"text_boxes\": [\n {\n \"text\": \"Dense label\",\n \"box_px\": [100, 100, 260, 24],\n \"font_size\": 24,\n }\n ],\n }\n\n normalized = normalize_manifest(manifest)\n text_box = normalized[\"text_boxes\"][0]\n\n self.assertLess(text_box[\"font_size\"], 24)\n self.assertEqual(24, text_box[\"_requested_font_size\"])\n\n def test_text_font_size_fit_can_be_disabled(self):\n manifest = {\n \"source\": {\"width_px\": 1600, \"height_px\": 900},\n \"slide\": {\"width\": 13.333, \"height\": 7.5},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 13.333, \"height\": 7.5},\n \"text_boxes\": [\n {\n \"text\": \"Locked label\",\n \"box_px\": [100, 100, 260, 24],\n \"font_size\": 24,\n \"fit_text\": False,\n }\n ],\n }\n\n normalized = normalize_manifest(manifest)\n\n self.assertEqual(24, normalized[\"text_boxes\"][0][\"font_size\"])\n\n def test_wrapped_text_fit_does_not_force_single_line_width(self):\n manifest = {\n \"source\": {\"width_px\": 1600, \"height_px\": 900},\n \"slide\": {\"width\": 13.333, \"height\": 7.5},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 13.333, \"height\": 7.5},\n \"text_boxes\": [\n {\n \"text\": \"Long body copy should wrap across multiple natural lines\",\n \"box_px\": [100, 100, 420, 120],\n \"font_size\": 18,\n \"wrap\": \"square\",\n }\n ],\n }\n\n normalized = normalize_manifest(manifest)\n\n self.assertGreater(normalized[\"text_boxes\"][0][\"font_size\"], 10)\n self.assertLessEqual(normalized[\"text_boxes\"][0][\"font_size\"], 18)\n\n def test_text_box_alignment_uses_drawingml_enum_values(self):\n xml = text_box_xml(\n 2,\n {\n \"text\": \"1\",\n \"left\": 0,\n \"top\": 0,\n \"width\": 1,\n \"height\": 1,\n \"align\": \"center\",\n \"valign\": \"middle\",\n },\n )\n\n self.assertIn('algn=\"ctr\"', xml)\n self.assertIn('anchor=\"ctr\"', xml)\n self.assertNotIn('algn=\"center\"', xml)\n self.assertNotIn('anchor=\"middle\"', xml)\n\n def test_preview_centers_text_inside_its_box(self):\n manifest = {\n \"source\": {\"width_px\": 200, \"height_px\": 200},\n \"slide\": {\"width\": 2, \"height\": 2, \"background\": \"#ffffff\"},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 2, \"height\": 2},\n \"preview_scale\": 100,\n \"text_boxes\": [\n {\n \"text\": \"1\",\n \"box_px\": [50, 50, 100, 100],\n \"font_size\": 48,\n \"fit_text\": False,\n \"color\": \"#000000\",\n \"align\": \"center\",\n \"valign\": \"middle\",\n }\n ],\n }\n\n ink_center = preview_ink_center(manifest)\n self.assertIsNotNone(ink_center)\n ink_center_x, ink_center_y = ink_center\n\n self.assertAlmostEqual(100, ink_center_x, delta=5)\n self.assertAlmostEqual(100, ink_center_y, delta=5)\n\n def test_preview_centers_mixed_size_runs_inside_their_box(self):\n preview_font = scalable_test_font()\n self.assertIsNotNone(preview_font)\n manifest = {\n \"source\": {\"width_px\": 200, \"height_px\": 200},\n \"slide\": {\"width\": 2, \"height\": 2, \"background\": \"#ffffff\"},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 2, \"height\": 2},\n \"preview_scale\": 100,\n \"text_boxes\": [\n {\n \"runs\": [\n {\"text\": \"1\", \"font_size\": 72},\n {\"text\": \"A\", \"font_size\": 12},\n ],\n \"box_px\": [50, 50, 100, 100],\n \"font_size\": 18,\n \"fit_text\": False,\n \"preview_font\": preview_font,\n \"color\": \"#000000\",\n \"align\": \"center\",\n \"valign\": \"middle\",\n }\n ],\n }\n\n ink_center = preview_ink_center(manifest)\n self.assertIsNotNone(ink_center)\n ink_center_x, ink_center_y = ink_center\n\n self.assertAlmostEqual(100, ink_center_x, delta=8)\n self.assertAlmostEqual(100, ink_center_y, delta=8)\n\n def test_preview_rotates_centered_text_around_the_box_center(self):\n manifest = {\n \"source\": {\"width_px\": 200, \"height_px\": 200},\n \"slide\": {\"width\": 2, \"height\": 2, \"background\": \"#ffffff\"},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 2, \"height\": 2},\n \"preview_scale\": 100,\n \"text_boxes\": [\n {\n \"text\": \"1\",\n \"box_px\": [50, 50, 100, 100],\n \"font_size\": 48,\n \"fit_text\": False,\n \"color\": \"#000000\",\n \"align\": \"center\",\n \"valign\": \"middle\",\n \"rotation\": 45,\n }\n ],\n }\n\n ink_center = preview_ink_center(manifest)\n self.assertIsNotNone(ink_center)\n ink_center_x, ink_center_y = ink_center\n\n self.assertAlmostEqual(100, ink_center_x, delta=8)\n self.assertAlmostEqual(100, ink_center_y, delta=8)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9539,"content_sha256":"57b8c87454356af14ada0eec8fcfcdfb77ec133604067ea30241ba83fd74e6e4"},{"filename":"tests/test_state_e2e.py","content":"from __future__ import annotations\n\nimport json\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nfrom support import (\n SCRIPTS,\n build_page,\n complete_visual_review_evidence,\n image2ppt_command,\n filled_arrow_manifest,\n write_json,\n)\n\n\nclass Image2PPTStateEndToEndTests(unittest.TestCase):\n def run_ok(self, command: list[str]) -> subprocess.CompletedProcess[str]:\n result = subprocess.run(command, text=True, capture_output=True, check=False)\n self.assertEqual(result.returncode, 0, result.stdout + result.stderr)\n return result\n\n def test_local_page_jobs_record_and_finalize(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n run = Path(name)\n page = run / \"pages\" / \"page_001\"\n manifest = filled_arrow_manifest(page)\n request = page / \"page_request.json\"\n write_json(\n request,\n {\n \"schema_version\": 1,\n \"page_id\": \"page_001\",\n \"page_index\": 1,\n \"page_dir\": str(page),\n \"source_image\": str(page / \"source.png\"),\n \"slide\": {\"width\": 13.333, \"height\": 7.5},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 13.333, \"height\": 7.5},\n },\n )\n write_json(run / \"notes_manifest.json\", {\"source\": None, \"notes\": []})\n write_json(\n run / \"deck_manifest.json\",\n {\n \"schema_version\": 1,\n \"run_id\": \"thin-profile-fixture\",\n \"job_dir\": str(run),\n \"page_count\": 1,\n \"slide\": {\"width\": 13.333, \"height\": 7.5},\n \"pages\": [\n {\n \"page_id\": \"page_001\",\n \"page_index\": 1,\n \"page_dir\": \"pages/page_001\",\n \"source_image\": \"pages/page_001/source.png\",\n \"manifest\": \"pages/page_001/manifest.json\",\n \"validation\": \"pages/page_001/validation.json\",\n }\n ],\n \"notes_manifest\": \"notes_manifest.json\",\n \"output\": \"final/deck_edited.pptx\",\n },\n )\n write_json(\n run / \"page_jobs.json\",\n {\n \"schema_version\": 1,\n \"run_id\": \"thin-profile-fixture\",\n \"run_status\": \"inputs_prepared\",\n \"max_concurrent_pages\": 1,\n \"pages\": [\n {\n \"page_id\": \"page_001\",\n \"page_index\": 1,\n \"status\": \"pending\",\n \"page_dir\": \"pages/page_001\",\n \"source\": \"pages/page_001/source.png\",\n \"page_request\": \"pages/page_001/page_request.json\",\n \"manifest\": \"pages/page_001/manifest.json\",\n \"validation\": \"pages/page_001/validation.json\",\n \"dispatch\": None,\n \"result\": None,\n \"accepted\": False,\n }\n ],\n },\n )\n\n prompt = page / \"worker-prompt.md\"\n self.run_ok(\n [\n sys.executable,\n str(SCRIPTS / \"build_page_worker_prompt.py\"),\n str(run),\n \"--page\",\n \"page_001\",\n \"--out\",\n str(prompt),\n ]\n )\n self.run_ok(\n [\n *image2ppt_command(),\n \"run\",\n \"dispatch\",\n str(run),\n \"--page\",\n \"page_001\",\n \"--agent-id\",\n \"main\",\n \"--prompt-file\",\n str(prompt),\n \"--local\",\n ]\n )\n build_page(page)\n pending_page_qa = subprocess.run(\n [sys.executable, str(SCRIPTS / \"run_image2ppt_qa.py\"), str(page)],\n text=True,\n capture_output=True,\n check=False,\n )\n self.assertEqual(pending_page_qa.returncode, 2, pending_page_qa.stdout + pending_page_qa.stderr)\n page_evidence = page / \"visual-review-evidence.json\"\n complete_visual_review_evidence(\n page / \"visual-review-evidence.template.json\", page_evidence\n )\n self.run_ok(\n [\n sys.executable,\n str(SCRIPTS / \"run_image2ppt_qa.py\"),\n str(page),\n \"--visual-review-status\",\n \"reviewed\",\n \"--visual-review-evidence\",\n str(page_evidence),\n \"--visual-review-notes\",\n \"fixture: source and render checked for arrow silhouette, direction, text, and bounds\",\n ]\n )\n self.run_ok([*image2ppt_command(), \"page\", \"contact-sheet\", str(page)])\n write_json(\n page / \"page_result.json\",\n {\n \"page_manifest\": \"manifest.json\",\n \"imagegen_jobs\": \"imagegen-jobs.json\",\n \"page_pptx\": \"page.pptx\",\n \"preview\": \"preview.png\",\n \"contact_sheet\": \"split_assets_contact.png\",\n \"validation\": \"validation.json\",\n \"page_result\": \"page_result.json\",\n },\n )\n self.run_ok(\n [\n *image2ppt_command(),\n \"run\",\n \"record\",\n str(run),\n \"--page\",\n \"page_001\",\n \"--agent-id\",\n \"main\",\n ]\n )\n self.run_ok([*image2ppt_command(), \"run\", \"finalize\", str(run)])\n pending_final_qa = subprocess.run(\n [sys.executable, str(SCRIPTS / \"run_final_image2ppt_qa.py\"), str(run)],\n text=True,\n capture_output=True,\n check=False,\n )\n self.assertEqual(pending_final_qa.returncode, 2, pending_final_qa.stdout + pending_final_qa.stderr)\n final_evidence = run / \"final\" / \"visual-review-evidence.json\"\n complete_visual_review_evidence(\n run / \"final\" / \"visual-review-evidence.template.json\", final_evidence\n )\n self.run_ok(\n [\n sys.executable,\n str(SCRIPTS / \"run_final_image2ppt_qa.py\"),\n str(run),\n \"--visual-review-status\",\n \"reviewed\",\n \"--visual-review-evidence\",\n str(final_evidence),\n \"--visual-review-notes\",\n \"fixture: final rendered slide checked against source for arrow, text, and page geometry\",\n ]\n )\n\n jobs = json.loads((run / \"page_jobs.json\").read_text(encoding=\"utf-8\"))\n self.assertEqual(jobs[\"run_status\"], \"complete\")\n self.assertEqual(jobs[\"pages\"][0][\"status\"], \"accepted\")\n self.assertFalse((run / \"image2ppt_jobs.json\").exists())\n self.assertTrue((run / \"final\" / \"image2ppt_qa.json\").is_file())\n report_text = (run / \"final\" / \"image2ppt_qa.json\").read_text(encoding=\"utf-8\")\n self.assertTrue(json.loads(report_text)[\"passed\"])\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8026,"content_sha256":"db76d8a754f4692018a4d7ca0ce9b9a6d081ddc1db4c0f252c78b408b7d7a249"},{"filename":"tests/test_structure_reconstruction.py","content":"from __future__ import annotations\n\nimport json\nimport re\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nimport zipfile\nfrom pathlib import Path\nfrom xml.etree import ElementTree as ET\n\nfrom PIL import Image\n\nfrom support import SCRIPTS, build_deck, build_page, write_json\n\n\nNS = {\n \"a\": \"http://schemas.openxmlformats.org/drawingml/2006/main\",\n \"p\": \"http://schemas.openxmlformats.org/presentationml/2006/main\",\n}\n\n\ndef run_ok(command: list[str]) -> None:\n result = subprocess.run(command, text=True, capture_output=True, check=False)\n if result.returncode != 0:\n raise AssertionError(result.stdout + result.stderr)\n\n\ndef graph_manifest(page: Path) -> Path:\n page.mkdir(parents=True)\n Image.new(\"RGB\", (800, 450), \"white\").save(page / \"source.png\")\n nodes = []\n node_shapes = []\n node_anchors = []\n for index in range(42):\n row, column = divmod(index, 7)\n x = 70 + column * 85\n y = 65 + row * 44\n item_id = f\"kg_node_{index + 1:02d}\"\n node_shapes.append(\n {\n \"id\": item_id,\n \"type\": \"ellipse\",\n \"box_px\": [x, y, 22, 22],\n \"fill\": \"#E8F2FF\",\n \"stroke\": \"#2563EB\",\n \"stroke_width\": 1.2,\n \"z_index\": 100 + index,\n }\n )\n nodes.append(\n {\n \"id\": f\"node-{index + 1:02d}\",\n \"manifest_id\": item_id,\n \"geometry\": \"circle\",\n \"source_center_px\": [x + 11, y + 11],\n \"source_size_px\": [22, 22],\n \"position_tolerance_px\": 1,\n \"size_tolerance_px\": 1,\n }\n )\n node_anchors.append(\n {\n \"id\": f\"anchor-{item_id}\",\n \"kind\": \"circle-node\",\n \"manifest_id\": item_id,\n \"source_bbox_px\": [x, y, 22, 22],\n }\n )\n\n edges = []\n edge_shapes = []\n edge_anchors = []\n for index in range(41):\n start = nodes[index][\"source_center_px\"]\n end = nodes[index + 1][\"source_center_px\"]\n item_id = f\"kg_edge_{index + 1:02d}\"\n dashed = index % 5 == 4\n shape = {\n \"id\": item_id,\n \"type\": \"line\",\n \"points_px\": [*start, *end],\n \"stroke\": \"#64748B\",\n \"stroke_width\": 1.0,\n \"connector\": \"straight\",\n \"start_arrow\": \"none\",\n \"end_arrow\": \"triangle\",\n \"z_index\": 20 + index,\n }\n if dashed:\n shape[\"dash\"] = \"dash\"\n edge_shapes.append(shape)\n edges.append(\n {\n \"id\": f\"edge-{index + 1:02d}\",\n \"manifest_id\": item_id,\n \"source_points_px\": [*start, *end],\n \"line_style\": \"dashed\" if dashed else \"solid\",\n \"direction\": \"end\",\n \"position_tolerance_px\": 1,\n }\n )\n edge_anchors.append(\n {\n \"id\": f\"anchor-{item_id}\",\n \"kind\": \"dashed-relationship\" if dashed else \"directed-connector\",\n \"manifest_id\": item_id,\n \"source_points_px\": [*start, *end],\n }\n )\n\n thin_arrow = {\n \"id\": \"standalone_thin_arrow\",\n \"type\": \"line\",\n \"points_px\": [100, 390, 300, 390],\n \"stroke\": \"#0F766E\",\n \"stroke_width\": 2.5,\n \"connector\": \"straight\",\n \"start_arrow\": \"none\",\n \"end_arrow\": \"triangle\",\n \"z_index\": 250,\n }\n filled_arrow = {\n \"id\": \"standalone_filled_arrow\",\n \"type\": \"shape\",\n \"preset\": \"rightArrow\",\n \"box_px\": [380, 365, 230, 55],\n \"fill\": \"#2563EB\",\n \"stroke\": \"none\",\n \"text\": \"下一阶段\",\n \"font_size\": 14,\n \"text_color\": \"#FFFFFF\",\n \"align\": \"center\",\n \"valign\": \"middle\",\n \"z_index\": 260,\n }\n graph_shape_ids = [item[\"id\"] for item in [*node_shapes, *edge_shapes]]\n manifest = {\n \"schema_version\": 1,\n \"slide\": {\"width\": 13.333, \"height\": 7.5, \"background\": \"#FFFFFF\"},\n \"content_box\": {\"left\": 0, \"top\": 0, \"width\": 13.333, \"height\": 7.5},\n \"source\": {\"path\": \"source.png\", \"width_px\": 800, \"height_px\": 450},\n \"page_strategy\": \"native-structure\",\n \"required_text\": [\"Knowledge Graph\", \"下一阶段\"],\n \"text_inventory\": [\"Knowledge Graph\", \"下一阶段\"],\n \"visual_inventory\": [\n {\"id\": \"graph\", \"description\": \"native structural measured knowledge graph\", \"decision\": \"native structure\"},\n {\"id\": \"arrows\", \"description\": \"native structural ordinary arrows\", \"decision\": \"native structure\"},\n ],\n \"background_strategy\": {\n \"mode\": \"native-or-script\",\n \"source_consistency_contract\": \"solid white source background\",\n \"removed_foreground\": [],\n \"comparison_note\": \"solid white background matched\",\n },\n \"quality_checks\": {\n \"font_size_calibrated\": True,\n \"visual_inventory_matched\": True,\n \"background_strategy_checked\": True,\n \"shape_corner_geometry_checked\": True,\n },\n \"text_boxes\": [\n {\n \"id\": \"title\",\n \"text\": \"Knowledge Graph\",\n \"box_px\": [30, 10, 400, 35],\n \"font_size\": 20,\n \"font_size_source\": \"measured\",\n \"color\": \"#111827\",\n \"z_index\": 300,\n }\n ],\n \"shapes\": [*edge_shapes, *node_shapes, thin_arrow, filled_arrow],\n \"images\": [],\n \"asset_provenance\": [],\n \"image2ppt_region_decomposition\": {\n \"schema_version\": \"image2ppt-region-decomposition-v1\",\n \"page_complexity\": \"structured\",\n \"source_size_px\": [800, 450],\n \"regions\": [\n {\n \"id\": \"title-region\",\n \"label\": \"Title\",\n \"source_bbox_px\": [0, 0, 800, 50],\n \"risk_level\": \"low\",\n \"strategy\": \"native-text-layout\",\n \"reason\": \"Measured native title.\",\n \"manifest_ids\": {\"shapes\": [], \"text_boxes\": [\"title\"], \"images\": []},\n \"protected_visual_anchors\": [],\n },\n {\n \"id\": \"knowledge-graph-region\",\n \"label\": \"42-node knowledge graph\",\n \"source_bbox_px\": [40, 50, 650, 300],\n \"risk_level\": \"high\",\n \"strategy\": \"native-object-decomposition\",\n \"reason\": \"All 42 circles and 41 directed relationships are individually measured.\",\n \"manifest_ids\": {\"shapes\": graph_shape_ids, \"text_boxes\": [], \"images\": []},\n \"protected_visual_anchors\": [*node_anchors, *edge_anchors],\n \"compound_diagram\": {\n \"kind\": \"knowledge-graph\",\n \"object_inventory_complete\": True,\n \"measurement_reviewed\": True,\n \"native_object_policy\": \"measured-simple-objects-native\",\n \"nodes\": nodes,\n \"edges\": edges,\n },\n },\n {\n \"id\": \"arrow-region\",\n \"label\": \"Arrow examples\",\n \"source_bbox_px\": [40, 350, 700, 90],\n \"risk_level\": \"medium\",\n \"strategy\": \"native-object-decomposition\",\n \"reason\": \"Ordinary arrows are single native objects.\",\n \"manifest_ids\": {\n \"shapes\": [thin_arrow[\"id\"], filled_arrow[\"id\"]],\n \"text_boxes\": [],\n \"images\": [],\n },\n \"protected_visual_anchors\": [\n {\n \"id\": \"anchor-standalone-thin\",\n \"kind\": \"directed-connector\",\n \"manifest_id\": thin_arrow[\"id\"],\n \"source_points_px\": thin_arrow[\"points_px\"],\n },\n {\n \"id\": \"anchor-standalone-filled\",\n \"kind\": \"route-line\",\n \"manifest_id\": filled_arrow[\"id\"],\n \"source_bbox_px\": filled_arrow[\"box_px\"],\n },\n ],\n },\n ],\n },\n }\n path = page / \"manifest.json\"\n write_json(path, manifest)\n write_json(page / \"imagegen-jobs.json\", {\"schema_version\": 1, \"jobs\": []})\n return path\n\n\ndef slide_xml(pptx: Path) -> ET.Element:\n with zipfile.ZipFile(pptx) as archive:\n return ET.fromstring(archive.read(\"ppt/slides/slide1.xml\"))\n\n\ndef structure_counts(pptx: Path) -> dict[str, int]:\n root = slide_xml(pptx)\n ellipses = 0\n filled = 0\n for shape in root.findall(\".//p:sp\", NS):\n geom = shape.find(\".//a:prstGeom\", NS)\n if geom is not None and geom.get(\"prst\") == \"ellipse\":\n ellipses += 1\n if geom is not None and geom.get(\"prst\") == \"rightArrow\":\n filled += 1\n return {\n \"circle_nodes\": ellipses,\n \"connectors\": len(root.findall(\".//p:cxnSp\", NS)),\n \"filled_arrows\": filled,\n \"pictures\": len(root.findall(\".//p:pic\", NS)),\n }\n\n\nclass StructureReconstructionTests(unittest.TestCase):\n def test_42_native_nodes_relations_and_atomic_arrows_survive_final_build(self) -> None:\n with tempfile.TemporaryDirectory() as tmp:\n run = Path(tmp)\n page = run / \"pages\" / \"page_001\"\n manifest = graph_manifest(page)\n page_pptx = build_page(page)\n region_report = page / \"region-report.json\"\n run_ok(\n [sys.executable, str(SCRIPTS / \"inspect_region_decomposition.py\"), \"--manifest\", str(manifest), \"--out\", str(region_report)]\n )\n page_arrow_report = page / \"arrow-postprocess.json\"\n run_ok(\n [sys.executable, str(SCRIPTS / \"postprocess_manifest_arrows.py\"), str(page_pptx), \"--manifest\", str(manifest), \"--report\", str(page_arrow_report)]\n )\n page_inspection = page / \"arrow-inspection.json\"\n run_ok(\n [sys.executable, str(SCRIPTS / \"inspect_arrow_atomicity.py\"), str(page_pptx), \"--manifest\", str(manifest), \"--out\", str(page_inspection)]\n )\n counts = structure_counts(page_pptx)\n self.assertEqual(counts[\"circle_nodes\"], 42)\n self.assertEqual(counts[\"connectors\"], 42) # 41 relations + one standalone thin arrow\n self.assertEqual(counts[\"filled_arrows\"], 1)\n self.assertEqual(counts[\"pictures\"], 0)\n region = json.loads(region_report.read_text(encoding=\"utf-8\"))\n self.assertTrue(region[\"passed\"], region)\n self.assertEqual(region[\"pages\"][0][\"region_count\"], 3)\n arrows = json.loads(page_inspection.read_text(encoding=\"utf-8\"))\n self.assertTrue(arrows[\"passed\"], arrows)\n self.assertEqual(arrows[\"summary\"][\"connectors\"], 42)\n self.assertEqual(arrows[\"summary\"][\"filled_arrows\"], 1)\n\n final = run / \"final\" / \"deck_edited.pptx\"\n write_json(\n run / \"deck_manifest.json\",\n {\n \"schema_version\": 1,\n \"job_dir\": str(run),\n \"page_count\": 1,\n \"slide\": {\"width\": 13.333, \"height\": 7.5},\n \"pages\": [{\"page_id\": \"page_001\", \"manifest\": \"pages/page_001/manifest.json\", \"source_image\": \"pages/page_001/source.png\"}],\n \"output\": \"final/deck_edited.pptx\",\n },\n )\n final.parent.mkdir(parents=True, exist_ok=True)\n build_deck(run / \"deck_manifest.json\", final)\n final_arrow_report = final.parent / \"arrow-postprocess.json\"\n run_ok(\n [sys.executable, str(SCRIPTS / \"postprocess_manifest_arrows.py\"), str(final), \"--run\", str(run), \"--report\", str(final_arrow_report)]\n )\n final_inspection = final.parent / \"arrow-inspection.json\"\n run_ok(\n [sys.executable, str(SCRIPTS / \"inspect_arrow_atomicity.py\"), str(final), \"--run\", str(run), \"--out\", str(final_inspection)]\n )\n self.assertEqual(structure_counts(final), counts)\n self.assertTrue(json.loads(final_inspection.read_text(encoding=\"utf-8\"))[\"passed\"])\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12822,"content_sha256":"b01ee9428fa2d099bd6d70b8ba2594cddcc73478744726005ffa47af7ca16c16"},{"filename":"tests/test_thin_profile.py","content":"from __future__ import annotations\n\nimport json\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nfrom support import SKILL_ROOT, SCRIPTS, write_json\n\n\nclass SingleRuntimeTests(unittest.TestCase):\n def test_only_one_state_and_manifest_lifecycle_is_defined(self) -> None:\n forbidden_files = [\n \"image2ppt_jobs.json\",\n \"image2ppt_plan.json\",\n \"source_reconstruction_plan.json\",\n \"image2ppt_page_result.json\",\n ]\n runtime_text = \"\\n\".join(\n path.read_text(encoding=\"utf-8\")\n for root in (SKILL_ROOT / \"cli\", SKILL_ROOT / \"scripts\")\n for path in root.rglob(\"*.py\")\n )\n for name in forbidden_files:\n self.assertNotIn(name, runtime_text)\n self.assertTrue((SKILL_ROOT / \"cli/image2ppt/runtime/deck_run_state.py\").is_file())\n self.assertTrue((SKILL_ROOT / \"cli/image2ppt/runtime/paddle_text_hints.py\").is_file())\n self.assertTrue((SKILL_ROOT / \"cli/image2ppt/runtime/text_hints.py\").is_file())\n\n def test_prompt_builder_is_complete_and_uses_absolute_local_cli(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n run = Path(name)\n page = run / \"pages\" / \"page_001\"\n page.mkdir(parents=True)\n source = page / \"source.png\"\n source.write_bytes(b\"fixture-path-only\")\n write_json(\n run / \"page_jobs.json\",\n {\"schema_version\": 1, \"pages\": [{\"page_id\": \"page_001\", \"page_dir\": \"pages/page_001\", \"status\": \"pending\"}]},\n )\n write_json(page / \"page_request.json\", {\"source_image\": str(source)})\n out = page / \"worker-prompt.md\"\n result = subprocess.run(\n [sys.executable, str(SCRIPTS / \"build_page_worker_prompt.py\"), str(run), \"--page\", \"page_001\", \"--out\", str(out)],\n text=True,\n capture_output=True,\n check=False,\n )\n self.assertEqual(result.returncode, 0, result.stdout + result.stderr)\n payload = json.loads(result.stdout)\n self.assertEqual(payload[\"state_owner\"], \"page_jobs.json\")\n self.assertEqual(Path(payload[\"prompt_template\"]), SKILL_ROOT / \"prompts\" / \"page-worker-base.md\")\n self.assertEqual(Path(payload[\"prompt_addendum\"]), SKILL_ROOT / \"prompts\" / \"page-worker.md\")\n prompt = out.read_text(encoding=\"utf-8\")\n self.assertIn(\"Rebuild one page for Image2PPT\", prompt)\n self.assertIn(str(SKILL_ROOT / \"cli/image2ppt/cli.py\"), prompt)\n self.assertIn(\"Every non-text foreground visual object must be separated\", prompt)\n self.assertIn(\"Execute the three steps in order\", prompt)\n self.assertIn(\"Do not invent an object-source strategy\", prompt)\n self.assertIn(\"Put icons/foreground objects onto one sparse asset sheet\", prompt)\n self.assertIn(\"Replace only the base prompt's final page build/validate sequence\", prompt)\n self.assertIn(\"3-5 semantic regions\", prompt)\n self.assertIn(\"image2ppt_region_decomposition\", prompt)\n self.assertIn(\"every node center/size and every edge endpoint/direction\", prompt)\n self.assertIn(\"complex local subparts that would visibly drift\", prompt)\n self.assertNotIn(\"{{RUN_DIR}}\", prompt)\n self.assertNotIn(\"{{CLI}}\", prompt)\n\n def test_skill_declares_local_owners_and_final_gate(self) -> None:\n skill = (SKILL_ROOT / \"SKILL.md\").read_text(encoding=\"utf-8\")\n self.assertIn(\"`page_jobs.json` as the only page-state source\", skill)\n self.assertIn(\"`pages/page_NNN/manifest.json` as the only page-content source\", skill)\n self.assertIn(\"PADDLE_OCR_TOKEN\", skill)\n self.assertIn(\"builtin-ink\", skill)\n self.assertIn(\"run_final_image2ppt_qa.py\", skill)\n self.assertIn(\"measured compound diagrams\", skill.lower())\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4059,"content_sha256":"7a221237a1365e69aa0f6d86d70ef698755f10d88d3b015fa1fd44ea5b20f4b9"},{"filename":"tests/test_visual_review_evidence.py","content":"from __future__ import annotations\n\nimport json\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nfrom support import SCRIPTS, build_page, complete_visual_review_evidence, filled_arrow_manifest\n\nsys.path.insert(0, str(SCRIPTS))\nfrom visual_review_evidence import expected_page, validate_evidence, write_template # noqa: E402\n\n\nclass VisualReviewEvidenceTests(unittest.TestCase):\n def test_template_requires_conditional_arrow_check(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n page = Path(name)\n manifest_path = filled_arrow_manifest(page)\n build_page(page)\n rendered = page / \"preview.png\"\n expected = [\n expected_page(\n page_id=\"page_001\",\n source=page / \"source.png\",\n rendered=rendered,\n manifest=json.loads(manifest_path.read_text(encoding=\"utf-8\")),\n )\n ]\n template = page / \"template.json\"\n write_template(template, expected)\n checks = json.loads(template.read_text(encoding=\"utf-8\"))[\"pages\"][0][\"checks\"]\n self.assertIn(\"arrow_geometry\", checks)\n\n def test_evidence_paths_bind_review_to_current_artifacts(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n page = Path(name)\n manifest_path = filled_arrow_manifest(page)\n build_page(page)\n expected = [\n expected_page(\n page_id=\"page_001\",\n source=page / \"source.png\",\n rendered=page / \"preview.png\",\n manifest=json.loads(manifest_path.read_text(encoding=\"utf-8\")),\n )\n ]\n evidence = page / \"visual-review-evidence.json\"\n write_template(evidence, expected)\n complete_visual_review_evidence(evidence, evidence)\n payload = json.loads(evidence.read_text(encoding=\"utf-8\"))\n payload[\"pages\"][0][\"rendered\"] = str(page / \"unrelated.png\")\n evidence.write_text(json.dumps(payload), encoding=\"utf-8\")\n _payload, errors = validate_evidence(evidence, expected)\n self.assertTrue(any(\"rendered path\" in error for error in errors))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2355,"content_sha256":"e47643a7c983d34893d7692d22bf505010411ac610b64b82b304513d68a1edd3"},{"filename":"tests/test_visual_review_runtime.py","content":"from __future__ import annotations\n\nimport json\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nfrom support import SCRIPTS, build_page, complete_visual_review_evidence, filled_arrow_manifest\n\nsys.path.insert(0, str(SCRIPTS))\nfrom visual_review_evidence import expected_page, validate_evidence # noqa: E402\n\n\nclass VisualReviewRuntimeTests(unittest.TestCase):\n \"\"\"Integration tests that require a real PowerPoint or LibreOffice renderer.\"\"\"\n\n def test_generic_review_notes_do_not_pass_without_structured_evidence(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n page = Path(name)\n filled_arrow_manifest(page)\n build_page(page)\n result = subprocess.run(\n [\n sys.executable,\n str(SCRIPTS / \"run_image2ppt_qa.py\"),\n str(page),\n \"--visual-review-status\",\n \"reviewed\",\n \"--visual-review-notes\",\n \"looks good\",\n ],\n text=True,\n capture_output=True,\n check=False,\n )\n self.assertEqual(result.returncode, 2, result.stdout + result.stderr)\n report = json.loads((page / \"image2ppt_qa.json\").read_text(encoding=\"utf-8\"))\n self.assertFalse(report[\"passed\"])\n self.assertTrue(any(\"visual review evidence\" in error for error in report[\"errors\"]))\n\n def test_evidence_hashes_bind_review_to_current_render(self) -> None:\n with tempfile.TemporaryDirectory() as name:\n page = Path(name)\n manifest_path = filled_arrow_manifest(page)\n build_page(page)\n pending = subprocess.run(\n [sys.executable, str(SCRIPTS / \"run_image2ppt_qa.py\"), str(page)],\n text=True,\n capture_output=True,\n check=False,\n )\n self.assertEqual(pending.returncode, 2, pending.stdout + pending.stderr)\n evidence = page / \"visual-review-evidence.json\"\n complete_visual_review_evidence(\n page / \"visual-review-evidence.template.json\", evidence\n )\n payload = json.loads(evidence.read_text(encoding=\"utf-8\"))\n payload[\"pages\"][0][\"rendered_sha256\"] = \"0\" * 64\n evidence.write_text(json.dumps(payload), encoding=\"utf-8\")\n expected = [\n expected_page(\n page_id=page.name,\n source=page / \"source.png\",\n rendered=page / \"render\" / \"rendered.png\",\n manifest=json.loads(manifest_path.read_text(encoding=\"utf-8\")),\n )\n ]\n _payload, errors = validate_evidence(evidence, expected)\n self.assertTrue(any(\"rendered_sha256\" in error for error in errors))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2976,"content_sha256":"255dea5b7f5bf13c2e797986694038f0c22267d25f0ffd31295313ccda836ad9"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Nature Image2PPT","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this directory as the complete runtime. Run deterministic actions only through:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py \u003ccommand> ...","type":"text"}]},{"type":"paragraph","content":[{"text":"Use Python 3.10 or later with ","type":"text"},{"text":"requirements.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" installed. When a dedicated environment exists, substitute ","type":"text"},{"text":"\u003cimage2ppt-root>/.venv/bin/python","type":"text","marks":[{"type":"code_inline"}]},{"text":" on macOS/Linux or ","type":"text"},{"text":"\u003cimage2ppt-root>/.venv/Scripts/python.exe","type":"text","marks":[{"type":"code_inline"}]},{"text":" on Windows for every ","type":"text"},{"text":"python","type":"text","marks":[{"type":"code_inline"}]},{"text":" command below. Do not continue after a failed ","type":"text"},{"text":"doctor","type":"text","marks":[{"type":"code_inline"}]},{"text":"; install only the reported missing dependency, then rerun it.","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not discover or invoke another Skill, CLI, Prompt, Schema, module, or state machine.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Read the local contracts progressively","type":"text"}]},{"type":"paragraph","content":[{"text":"Always read ","type":"text"},{"text":"references/workflow.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Read ","type":"text"},{"text":"references/runtime-dependencies.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" only for setup or ","type":"text"},{"text":"doctor","type":"text","marks":[{"type":"code_inline"}]},{"text":" failures, and read ","type":"text"},{"text":"references/ocr-text-hints-contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" only when choosing or troubleshooting OCR.","type":"text"}]},{"type":"paragraph","content":[{"text":"Before writing a page manifest, read ","type":"text"},{"text":"references/page-decision-tree.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"references/manifest-schema.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Add only the references needed by that page:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"structured or compound page: ","type":"text"},{"text":"references/region-decomposition.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"references/object-routing.md","type":"text","marks":[{"type":"code_inline"}]},{"text":";","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"arrows: ","type":"text"},{"text":"references/manifest-arrow-extension.md","type":"text","marks":[{"type":"code_inline"}]},{"text":";","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"raster assets or image-backend work: ","type":"text"},{"text":"references/assets-provenance-contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Before accepting or delivering output, read ","type":"text"},{"text":"references/qa-contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Preserve the single source of truth","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat ","type":"text"},{"text":"page_jobs.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the only page-state source.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat each ","type":"text"},{"text":"pages/page_NNN/manifest.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the only page-content source.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat ","type":"text"},{"text":"deck_manifest.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the final-assembly source.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use only ","type":"text"},{"text":"prepare","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"run next/dispatch/record/reset/hints/finalize","type":"text","marks":[{"type":"code_inline"}]},{"text":", and the page commands in the local CLI for stateful lifecycle operations.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keep semantic-region evidence in ","type":"text"},{"text":"manifest.json.image2ppt_region_decomposition","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never create a second job file, reconstruction plan, OCR normalizer, page controller, packager, or finalize path.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Let supplemental QA report failures; never let it mutate lifecycle state.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Keep every write inside its owner directory","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Page build, validation, hints, and QA may read and write only inside that page directory. Manifest paths, recorded assets, formulas, reports, previews, and ","type":"text"},{"text":"--out","type":"text","marks":[{"type":"code_inline"}]},{"text":" overrides must not use ","type":"text"},{"text":"..","type":"text","marks":[{"type":"code_inline"}]},{"text":", symlinks, or absolute paths to escape it. The sole external-input exception is an explicit image-tool result supplied to ","type":"text"},{"text":"image import","type":"text","marks":[{"type":"code_inline"}]},{"text":" or as ","type":"text"},{"text":"process-sheet --asset-sheet-source","type":"text","marks":[{"type":"code_inline"}]},{"text":"; it is copied into the page before becoming a build dependency.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run-level manifests and final outputs must remain inside the prepared run directory. Finalization rebuilds into a same-directory temporary file and publishes it atomically only after a successful build.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat any boundary rejection as a hard failure; do not copy the rejected file back into scope and present it as runtime output.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Preserve pre-migration behavior","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat self-containment as a path/import/entrypoint migration, not a redesign of reconstruction behavior.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate each worker Prompt from the complete local base layer plus the preserved Image2PPT profile layer. Do not condense, reinterpret, or replace either layer.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefer the previously validated visual strategy when several routes satisfy the contracts. Keep simple measured objects native and retain bounded complex assets wherever a native redraw would reduce fidelity.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never re-author an accepted baseline page merely to prove runtime independence.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Run the workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Image backend selection","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"builtin-imagegen","type":"text","marks":[{"type":"code_inline"}]},{"text":" when the agent runtime exposes ","type":"text"},{"text":"image_gen.imagegen","type":"text","marks":[{"type":"code_inline"}]},{"text":"; it is the preferred backend because the worker can inspect edit inputs and import the explicit local result. Use the CLI image contract only when the built-in tool is unavailable, errors, cannot read an input, or returns no valid local output. A missing optional argument such as model, mask, size, quality, or output path never authorizes fallback. Record the actual producer and permitted fallback reason in ","type":"text"},{"text":"imagegen-jobs.json","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"The CLI image contract is provider-neutral at the transport boundary. Select ","type":"text"},{"text":"codex-oauth","type":"text","marks":[{"type":"code_inline"}]},{"text":" only for GPT Image model ids. Select ","type":"text"},{"text":"openai-compatible-api","type":"text","marks":[{"type":"code_inline"}]},{"text":" for any provider-specific model whose endpoint implements the OpenAI Images-compatible ","type":"text"},{"text":"/images/generations","type":"text","marks":[{"type":"code_inline"}]},{"text":" and/or ","type":"text"},{"text":"/images/edits","type":"text","marks":[{"type":"code_inline"}]},{"text":" schema. Do not infer the image backend from the task's language model. Use an explicit backend when provenance matters; ","type":"text"},{"text":"auto","type":"text","marks":[{"type":"code_inline"}]},{"text":" uses Codex OAuth only for compatible GPT Image ids and otherwise selects the configured API without sending Codex OAuth credentials to third parties.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Preflight and OCR choice","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py doctor --json","type":"text"}]},{"type":"paragraph","content":[{"text":"Use Baidu AI Studio ","type":"text"},{"text":"PADDLE_OCR_TOKEN","type":"text","marks":[{"type":"code_inline"}]},{"text":" when configured. If it is absent, tell the user once that the local ","type":"text"},{"text":"builtin-ink","type":"text","marks":[{"type":"code_inline"}]},{"text":" fallback measures text geometry but does not recognize characters; offer the configuration path in ","type":"text"},{"text":"references/ocr-text-hints-contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Respect an offline-only choice.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Prepare one run","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py prepare \u003cinput...> \\\n --out-root output/image2ppt --image-backend builtin-imagegen","type":"text"}]},{"type":"paragraph","content":[{"text":"To pin a configured third-party provider/model for auditable provenance, prepare with ","type":"text"},{"text":"--image-backend openai-compatible-api","type":"text","marks":[{"type":"code_inline"}]},{"text":". The run contract records the exact ","type":"text"},{"text":"IMAGE2PPT_IMAGE_MODEL","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the active project config or environment; it does not substitute a GPT Image default merely because no ","type":"text"},{"text":"--model","type":"text","marks":[{"type":"code_inline"}]},{"text":" flag was passed.","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"--no-text-hints","type":"text","marks":[{"type":"code_inline"}]},{"text":" only when OCR processing is intentionally disabled. Regenerate hints without creating a new run when needed:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py run hints \u003crun-dir>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Advance and claim pages","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py run next \u003crun-dir> --json\npython \u003cimage2ppt-root>/scripts/build_page_worker_prompt.py \\\n \u003crun-dir> --page \u003cpage-id> --out \u003cabsolute-page-dir>/worker-prompt.md\npython \u003cimage2ppt-root>/cli/image2ppt/cli.py run dispatch \\\n \u003crun-dir> --page \u003cpage-id> --agent-id \u003cid> --prompt-file \u003cabsolute-prompt>","type":"text"}]},{"type":"paragraph","content":[{"text":"For exactly one page, claim it with ","type":"text"},{"text":"--local","type":"text","marks":[{"type":"code_inline"}]},{"text":" and reconstruct it in the current agent. For multiple pages, dispatch independent page workers up to the capacity in ","type":"text"},{"text":"page_jobs.json","type":"text","marks":[{"type":"code_inline"}]},{"text":". Do not reset a live worker merely because it is slow.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Reconstruct and gate each page","type":"text"}]},{"type":"paragraph","content":[{"text":"Plan a structured page as 3–5 semantic regions and route each region independently. Use measured compound diagrams: measure every node, relation, and protected anchor. Keep measurable circles, cards, straight/dashed relations, and simple connectors native. Use bounded transparent assets only for complex local subparts.","type":"text"}]},{"type":"paragraph","content":[{"text":"Represent a thin arrow as one connector with its arrowhead on the same object. Represent a filled arrow as one Arrow AutoShape, with centered label text inside the same object. Never construct an ordinary arrow from a line plus triangle and never flatten a whole knowledge graph into one image.","type":"text"}]},{"type":"paragraph","content":[{"text":"Write new page manifests with ","type":"text"},{"text":"schema_version: 2","type":"text","marks":[{"type":"code_inline"}]},{"text":". Use structured ","type":"text"},{"text":"visual_inventory","type":"text","marks":[{"type":"code_inline"}]},{"text":" items with explicit ","type":"text"},{"text":"kind","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"representation","type":"text","marks":[{"type":"code_inline"}]},{"text":" values, and write a concrete ","type":"text"},{"text":"quality_evidence","type":"text","marks":[{"type":"code_inline"}]},{"text":" observation for every required quality check. Formula rendering is a hard gate: a missing engine, converter, or failed compile must keep the page failed unless the user explicitly approves that exact formula exception and the manifest records both ","type":"text"},{"text":"user_approved_exception: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" and a concrete ","type":"text"},{"text":"approval_note","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"The worker Prompt performs the deterministic sequence. Its final gates are:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py page build \u003cpage-dir>\npython \u003cimage2ppt-root>/scripts/run_image2ppt_qa.py \u003cpage-dir>\n# The first run writes visual-review-evidence.template.json and remains pending.\n# Inspect source.png against render/rendered.png, copy and complete the template\n# as visual-review-evidence.json, repair if needed, then:\npython \u003cimage2ppt-root>/scripts/run_image2ppt_qa.py \u003cpage-dir> \\\n --visual-review-status reviewed \\\n --visual-review-evidence \u003cpage-dir>/visual-review-evidence.json\npython \u003cimage2ppt-root>/cli/image2ppt/cli.py page contact-sheet \u003cpage-dir>","type":"text"}]},{"type":"paragraph","content":[{"text":"The evidence file must cover the current source/render hashes and every required check with a specific observation. ","type":"text"},{"text":"--visual-review-notes","type":"text","marks":[{"type":"code_inline"}]},{"text":" is optional context and cannot substitute for the evidence file.","type":"text"}]},{"type":"paragraph","content":[{"text":"Record only after standard validation and the Image2PPT region, arrow, and rendered gates pass:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py run record \\\n \u003crun-dir> --page \u003cpage-id> --agent-id \u003cid>","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the same ","type":"text"},{"text":"run reset","type":"text","marks":[{"type":"code_inline"}]},{"text":" → dispatch → record lifecycle to repair rejected pages.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Finalize and revalidate the rebuilt deck","type":"text"}]},{"type":"paragraph","content":[{"text":"When ","type":"text"},{"text":"run next","type":"text","marks":[{"type":"code_inline"}]},{"text":" reports ","type":"text"},{"text":"finalize","type":"text","marks":[{"type":"code_inline"}]},{"text":", run:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python \u003cimage2ppt-root>/cli/image2ppt/cli.py run finalize \u003crun-dir>\npython \u003cimage2ppt-root>/scripts/run_final_image2ppt_qa.py \u003crun-dir>\n# The first run writes final/visual-review-evidence.template.json and remains pending.\n# Inspect every rendered slide, complete final/visual-review-evidence.json, then:\npython \u003cimage2ppt-root>/scripts/run_final_image2ppt_qa.py \u003crun-dir> \\\n --visual-review-status reviewed \\\n --visual-review-evidence \u003crun-dir>/final/visual-review-evidence.json","type":"text"}]},{"type":"paragraph","content":[{"text":"Finalize rebuilds from page manifests, preserves source speaker notes, validates the package, and writes the output recorded by ","type":"text"},{"text":"deck_manifest.json","type":"text","marks":[{"type":"code_inline"}]},{"text":". Final QA reapplies manifest arrows, verifies arrow atomicity and compound structure, renders every slide, checks speaker-note integrity, and writes ","type":"text"},{"text":"final/image2ppt_qa.json","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Deliver","type":"text"}]},{"type":"paragraph","content":[{"text":"Return the final PPTX path, standard final validation, and ","type":"text"},{"text":"final/image2ppt_qa.json","type":"text","marks":[{"type":"code_inline"}]},{"text":". Report which complex visuals remain replaceable bitmap assets. Do not call the deck complete while any page/final gate is pending or failed.","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"nature-image2ppt","author":"@skillopedia","source":{"stars":51879,"repo_name":"nature-skills","origin_url":"https://github.com/Yuan1z0825/nature-skills/blob/HEAD/skills/nature-image2ppt/SKILL.md","repo_owner":"Yuan1z0825","body_sha256":"0b49331f7b97e0751248c84506cada638f29ff44bb3023686f89a9f858748e9e","cluster_key":"c9df4c5dc3262cb4a0f016fa954da9858e9070509fae147d8b0589351cfeb23b","clean_bundle":{"format":"clean-skill-bundle-v1","source":"Yuan1z0825/nature-skills/skills/nature-image2ppt/SKILL.md","text_bytes":789006,"source_commit":"69817e4dad8bfc52fa4e59a9d8355e4264a4d3a7","skills_sh_rank":3050,"attachment_count":88,"text_attachments":88,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/nature-image2ppt/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"documents-office","category_label":"Documents"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"documents-office","import_tag":"skills-sh-top20k-2026-11","description":"Reconstruct slide images, screenshots, scanned PDFs, or image-only PPTX files as object-level editable PowerPoint. Use for 图片转可编辑PPT、截图还原PPT and diagram reconstruction; not authoring a new deck from research notes."}},"renderedAt":1793762259956}

Nature Image2PPT Use this directory as the complete runtime. Run deterministic actions only through: Use Python 3.10 or later with installed. When a dedicated environment exists, substitute on macOS/Linux or on Windows for every command below. Do not continue after a failed ; install only the reported missing dependency, then rerun it. Do not discover or invoke another Skill, CLI, Prompt, Schema, module, or state machine. Read the local contracts progressively Always read . Read only for setup or failures, and read only when choosing or troubleshooting OCR. Before writing a page manifest, rea…