From 38d6379f4b0c9484a7c94d9e8abd0e1f8eaed2e1 Mon Sep 17 00:00:00 2001 From: AIGC Director Kit contributors Date: Wed, 12 Aug 2026 16:04:57 +0800 Subject: [PATCH] add one-command example verification --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 5 + CONTRIBUTING.md | 1 + README.md | 21 +++ docs/feedback-template.md | 8 + pyproject.toml | 2 +- run_examples.bat | 34 +--- src/aigc_director_kit/__init__.py | 5 +- src/aigc_director_kit/cli.py | 29 +++ src/aigc_director_kit/verification.py | 254 ++++++++++++++++++++++++++ tests/test_cli.py | 10 + tests/test_verification.py | 42 +++++ tests/test_version.py | 2 +- 13 files changed, 379 insertions(+), 35 deletions(-) create mode 100644 src/aigc_director_kit/verification.py create mode 100644 tests/test_verification.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a6ece6..bf1c004 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: - name: Validate public examples run: | + python -m aigc_director_kit verify-examples --json python -m aigc_director_kit validate-plan examples/shot_plan.json --json python -m aigc_director_kit validate-plan examples/one_take_previs_case.json --json python -m aigc_director_kit validate-plan examples/film_contract_handoff_case.json --json diff --git a/CHANGELOG.md b/CHANGELOG.md index 58a187c..4b920e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CineThread Changelog +## [0.2.1] - 2026-08-12 + +- Added `verify-examples`, a cross-platform, privacy-safe one-command verification report for every public example. +- Updated the Windows launcher, quick start, contribution guide, feedback template, and CI to use the new verification path. + ## [0.2.0] - 2026-08-12 - Added the `aigc-director-skill-workflow` handoff contract and schema. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30796e1..c23a3d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ ## 开始 ```powershell +python -m aigc_director_kit verify-examples --json python -m unittest discover -s tests -v python -m aigc_director_kit validate-plan examples/shot_plan.json python -m aigc_director_kit list-actions --library examples/action_library.json --query 跑 diff --git a/README.md b/README.md index f4a85b4..fc36fde 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,26 @@ versioned JSON contract identifiers remain `aigc-director-kit` in the 0.x series for compatibility. +## Verify a checkout in one command + +After cloning or extracting this repository, run the verification from the +repository folder: + +```powershell +# Windows terminal (when the Python launcher is installed) +py -3 -m aigc_director_kit verify-examples --json + +# macOS or Linux +python3 -m aigc_director_kit verify-examples --json +``` + +It checks every public plan, workflow, prompt-pack, QC-boundary, action, and +runtime-handoff example, and returns a privacy-safe report with relative paths +only. It does not render or generate media. For a real bug report, review the +output first and use the [verified feedback template](docs/feedback-template.md). +Windows users who do not know which Python command to use can instead double-click +[`run_examples.bat`](run_examples.bat); it detects common Python installations. + ## Skill workflow handoff Local AIGC Skills can hand a sanitized workflow packet to this repository: @@ -94,6 +114,7 @@ Windows 用户也可以直接双击 [`run_examples.bat`](run_examples.bat) 查 命令行方式: ```powershell +python -m aigc_director_kit verify-examples --json python -m aigc_director_kit validate-plan examples/shot_plan.json python -m aigc_director_kit list-actions --library examples/action_library.json --query 跑 python -m aigc_director_kit compile-action ` diff --git a/docs/feedback-template.md b/docs/feedback-template.md index b19d621..920b98e 100644 --- a/docs/feedback-template.md +++ b/docs/feedback-template.md @@ -2,6 +2,14 @@ 只填写实际运行过的结果,不要为了活动申请制造反馈、Stars、Issue 或 PR。 +建议先在克隆或解压后的仓库目录运行: + +```powershell +python -m aigc_director_kit verify-examples --json +``` + +它只输出相对路径和通用环境信息;在公开前仍请自行检查输出,删除不应公开的内容。 + ```text 版本/commit: 运行环境: diff --git a/pyproject.toml b/pyproject.toml index 1321693..8226875 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aigc-director-kit" -version = "0.2.0" +version = "0.2.1" description = "CineThread: deterministic creative handoffs for AI animation, storyboards, and previs." readme = "README.md" requires-python = ">=3.10" diff --git a/run_examples.bat b/run_examples.bat index 1beaab3..8e66c7b 100644 --- a/run_examples.bat +++ b/run_examples.bat @@ -39,38 +39,8 @@ if errorlevel 1 goto :missing_python set "PYTHONPATH=%CD%\src" -echo === Validate shot plan === -%PYTHON_CMD% -m aigc_director_kit validate-plan examples\shot_plan.json -if errorlevel 1 goto :failed - -echo. -echo === Search action catalog === -%PYTHON_CMD% -m aigc_director_kit list-actions --library examples\action_library.json --query run -if errorlevel 1 goto :failed - -echo. -echo === Compile bounded action request === -%PYTHON_CMD% -m aigc_director_kit compile-action --library examples\action_library.json --text "run quick stop, blend 0.2s, fast, in place" -if errorlevel 1 goto :failed - -echo. -echo === Validate Skill workflow handoff === -%PYTHON_CMD% -m aigc_director_kit validate-workflow examples\skill_workflow_case.json --library examples\action_library.json -if errorlevel 1 goto :failed - -echo. -echo === Validate prompt pack === -%PYTHON_CMD% -m aigc_director_kit validate-prompt-pack examples\prompt_pack_case.json --plan examples\one_take_previs_case.json -if errorlevel 1 goto :failed - -echo. -echo === Validate QC evidence boundary === -%PYTHON_CMD% -m aigc_director_kit validate-qc-report examples\qc_report_unverified_case.json -if errorlevel 1 goto :failed - -echo. -echo === Build runtime adapter handoff === -%PYTHON_CMD% -m aigc_director_kit build-runtime-handoff examples\skill_workflow_case.json --library examples\action_library.json --adapter optional-runtime-adapter +echo === Verify public CineThread examples === +%PYTHON_CMD% -m aigc_director_kit verify-examples --root . if errorlevel 1 goto :failed echo. diff --git a/src/aigc_director_kit/__init__.py b/src/aigc_director_kit/__init__.py index 22cfdc9..a989fc5 100644 --- a/src/aigc_director_kit/__init__.py +++ b/src/aigc_director_kit/__init__.py @@ -5,14 +5,17 @@ from .prompt import validate_prompt_pack, validate_prompt_pack_file from .qc import validate_qc_report, validate_qc_report_file from .runtime import build_runtime_handoff, build_runtime_handoff_file +from .verification import find_project_root, run_public_example_verification from .workflow import validate_workflow, validate_workflow_file __all__ = [ "ValidationResult", "compile_action_request", + "find_project_root", "list_actions", "load_action_library", "load_plan", + "run_public_example_verification", "validate_plan", "validate_plan_file", "validate_prompt_pack", @@ -25,4 +28,4 @@ "validate_workflow_file", ] -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/src/aigc_director_kit/cli.py b/src/aigc_director_kit/cli.py index b696e5d..faa9fd0 100644 --- a/src/aigc_director_kit/cli.py +++ b/src/aigc_director_kit/cli.py @@ -13,6 +13,7 @@ from .prompt import validate_prompt_pack_file from .qc import validate_qc_report_file from .runtime import build_runtime_handoff_file +from .verification import run_public_example_verification from .workflow import validate_workflow_file @@ -61,6 +62,17 @@ def _build_parser() -> argparse.ArgumentParser: handoff.add_argument("--output", type=Path) handoff.add_argument("--json", action="store_true", dest="as_json") + verify = subparsers.add_parser( + "verify-examples", + help="Run a privacy-safe, cross-platform verification of the public examples.", + ) + verify.add_argument( + "--root", + type=Path, + help="CineThread source checkout; defaults to the current directory or a parent.", + ) + verify.add_argument("--json", action="store_true", dest="as_json") + search = subparsers.add_parser("list-actions", help="Search an action catalog.") search.add_argument("--library", required=True, type=Path) search.add_argument("--query", default="") @@ -163,6 +175,23 @@ def main(argv: list[str] | None = None) -> int: print(f"mode: {packet['runtime']['mode']}") return 0 if packet["valid"] else 2 + if args.command == "verify-examples": + report = run_public_example_verification(args.root) + if args.as_json: + _dump(report) + else: + print(f"valid: {'yes' if report['valid'] else 'no'}") + print(f"checks: {report['summary']['check_count']}") + print(f"passed_checks: {report['summary']['passed_check_count']}") + for check in report["checks"]: + print(f"{'ok' if check['valid'] else 'error'}: {check['id']}") + for error in check["errors"]: + print(f"error: {error}") + for warning in check["warnings"]: + print(f"warning: {warning}") + print(f"boundary: {report['boundary']}") + return 0 if report["valid"] else 2 + if args.command == "list-actions": library = load_action_library(args.library) _dump(list_actions(library, args.query, category=args.category, limit=args.limit)) diff --git a/src/aigc_director_kit/verification.py b/src/aigc_director_kit/verification.py new file mode 100644 index 0000000..c593469 --- /dev/null +++ b/src/aigc_director_kit/verification.py @@ -0,0 +1,254 @@ +"""Cross-platform verification of CineThread's public examples.""" + +from __future__ import annotations + +import json +import platform +import sys +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from .actions import compile_action_request, list_actions, load_action_library +from .contract import ValidationResult, validate_plan_file +from .prompt import validate_prompt_pack_file +from .qc import validate_qc_report_file +from .runtime import build_runtime_handoff_file +from .workflow import validate_workflow_file + + +def find_project_root(root: Path | None = None) -> Path: + """Find a CineThread source checkout without exposing local paths in errors.""" + + start = (root or Path.cwd()).resolve() + candidates = (start,) if root is not None else (start, *start.parents) + for candidate in candidates: + if (candidate / "pyproject.toml").is_file() and (candidate / "examples").is_dir(): + return candidate + raise FileNotFoundError( + "Could not find a CineThread source checkout. Run this command from a cloned " + "or extracted checkout, or pass --root ." + ) + + +def _relative_path(root: Path, path: Path) -> str: + return path.relative_to(root).as_posix() + + +def _sanitize_text(value: object, root: Path) -> str: + text = str(value) + native_root = str(root) + return text.replace(native_root, ".").replace(native_root.replace("\\", "/"), ".") + + +def _validation_check( + check_id: str, + root: Path, + path: Path, + validate: Callable[[], ValidationResult], +) -> dict[str, Any]: + try: + result = validate() + return { + "id": check_id, + "path": _relative_path(root, path), + "valid": result.valid, + "summary": result.summary, + "errors": [_sanitize_text(error, root) for error in result.errors], + "warnings": [_sanitize_text(warning, root) for warning in result.warnings], + } + except (OSError, ValueError, json.JSONDecodeError) as exc: + return { + "id": check_id, + "path": _relative_path(root, path), + "valid": False, + "summary": {}, + "errors": [_sanitize_text(exc, root)], + "warnings": [], + } + + +def _callable_check( + check_id: str, + root: Path, + run: Callable[[], dict[str, Any]], +) -> dict[str, Any]: + try: + return run() + except (OSError, ValueError, json.JSONDecodeError) as exc: + return { + "id": check_id, + "valid": False, + "summary": {}, + "errors": [_sanitize_text(exc, root)], + "warnings": [], + } + + +def _json_fixture_check(root: Path) -> dict[str, Any]: + paths = sorted((root / "examples").glob("*.json")) + sorted((root / "schemas").glob("*.json")) + try: + for path in paths: + json.loads(path.read_text(encoding="utf-8")) + return { + "id": "json-fixtures", + "valid": True, + "summary": {"parsed_file_count": len(paths)}, + "errors": [], + "warnings": [], + } + except (OSError, ValueError, json.JSONDecodeError) as exc: + return { + "id": "json-fixtures", + "valid": False, + "summary": {}, + "errors": [_sanitize_text(exc, root)], + "warnings": [], + } + + +def run_public_example_verification(root: Path | None = None) -> dict[str, Any]: + """Run deterministic checks over all public examples in a source checkout. + + The report contains relative paths and generic environment data so it can be + safely copied into a public issue after the user reviews it. + """ + + project_root = find_project_root(root) + examples = project_root / "examples" + library_path = examples / "action_library.json" + + checks: list[dict[str, Any]] = [ + _validation_check( + "shot-plan", + project_root, + examples / "shot_plan.json", + lambda: validate_plan_file(examples / "shot_plan.json"), + ), + _validation_check( + "one-take-previs-plan", + project_root, + examples / "one_take_previs_case.json", + lambda: validate_plan_file(examples / "one_take_previs_case.json"), + ), + _validation_check( + "film-contract-handoff-plan", + project_root, + examples / "film_contract_handoff_case.json", + lambda: validate_plan_file(examples / "film_contract_handoff_case.json"), + ), + _validation_check( + "multi-actor-prop-plan", + project_root, + examples / "multi_actor_prop_case.json", + lambda: validate_plan_file(examples / "multi_actor_prop_case.json"), + ), + _validation_check( + "skill-workflow", + project_root, + examples / "skill_workflow_case.json", + lambda: validate_workflow_file(examples / "skill_workflow_case.json", library_path), + ), + _validation_check( + "prompt-pack", + project_root, + examples / "prompt_pack_case.json", + lambda: validate_prompt_pack_file( + examples / "prompt_pack_case.json", + examples / "one_take_previs_case.json", + ), + ), + _validation_check( + "qc-evidence-boundary", + project_root, + examples / "qc_report_unverified_case.json", + lambda: validate_qc_report_file(examples / "qc_report_unverified_case.json"), + ), + ] + + def verify_action_catalog() -> dict[str, Any]: + library = load_action_library(library_path) + matches = list_actions(library, "run") + matched_ids = [str(item["id"]) for item in matches] + valid = "run_quick_stop" in matched_ids + return { + "id": "action-catalog-search", + "valid": valid, + "summary": {"matched_action_ids": matched_ids}, + "errors": [] if valid else ["Expected action 'run_quick_stop' was not found."], + "warnings": [], + } + + def verify_action_compilation() -> dict[str, Any]: + library = load_action_library(library_path) + result = compile_action_request( + "run quick stop, blend 0.2s, fast, in place", + library, + ) + matched_ids = [str(item["id"]) for item in result["matched_actions"]] + valid = bool(result["valid"]) and matched_ids == ["run_quick_stop"] + return { + "id": "action-request-compilation", + "valid": valid, + "summary": {"matched_action_ids": matched_ids}, + "errors": [] + if valid + else [_sanitize_text(result.get("error", "Action request did not compile."), project_root)], + "warnings": [], + } + + def verify_runtime_handoff() -> dict[str, Any]: + packet = build_runtime_handoff_file( + examples / "skill_workflow_case.json", + library_path, + adapter_name="optional-runtime-adapter", + ) + valid = bool(packet["valid"]) + validation = packet.get("validation", {}) + raw_errors = validation.get("errors", []) if isinstance(validation, dict) else [] + return { + "id": "runtime-handoff", + "valid": valid, + "summary": { + "adapter": packet["runtime"]["adapter"], + "mode": packet["runtime"]["mode"], + "compiled_action_count": len(packet["compiled_action_requests"]), + }, + "errors": [] + if valid + else [ + _sanitize_text(error, project_root) + for error in (raw_errors or ["Runtime handoff did not validate."]) + ], + "warnings": [], + } + + checks.extend( + [ + _callable_check("action-catalog-search", project_root, verify_action_catalog), + _callable_check("action-request-compilation", project_root, verify_action_compilation), + _callable_check("runtime-handoff", project_root, verify_runtime_handoff), + _json_fixture_check(project_root), + ] + ) + + return { + "tool": "CineThread", + "package": "aigc-director-kit", + "environment": { + "python": platform.python_version(), + "implementation": platform.python_implementation(), + "platform": platform.system(), + "python_executable": Path(sys.executable).name, + }, + "valid": all(check["valid"] for check in checks), + "summary": { + "check_count": len(checks), + "passed_check_count": sum(1 for check in checks if check["valid"]), + }, + "checks": checks, + "boundary": ( + "This verifies public JSON contracts and deterministic dry-run handoffs only. " + "It does not render, generate, open media, or perform final video QC." + ), + } diff --git a/tests/test_cli.py b/tests/test_cli.py index 0ee7758..7f59ac1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -15,6 +15,7 @@ def test_windows_example_runner_has_portable_python_fallbacks(self) -> None: self.assertIn("AIGC_DIRECTOR_PYTHON", runner) self.assertIn("%LocalAppData%\\Programs\\Python\\Python*\\python.exe", runner) self.assertIn("Python 3.10 or newer", runner) + self.assertIn("verify-examples --root .", runner) def test_validate_plan_json_mode(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: @@ -39,6 +40,15 @@ def test_compile_action_refuses_overwrite(self) -> None: ) self.assertEqual(code, 1) + def test_verify_examples_json_mode(self) -> None: + self.assertEqual( + main(["verify-examples", "--root", str(ROOT), "--json"]), + 0, + ) + + def test_verify_examples_rejects_an_invalid_explicit_root(self) -> None: + self.assertEqual(main(["verify-examples", "--root", str(ROOT / "missing")]), 1) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_verification.py b/tests/test_verification.py new file mode 100644 index 0000000..5592e6e --- /dev/null +++ b/tests/test_verification.py @@ -0,0 +1,42 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from aigc_director_kit.verification import find_project_root, run_public_example_verification + + +ROOT = Path(__file__).resolve().parents[1] + + +class PublicExampleVerificationTests(unittest.TestCase): + def test_verifies_all_public_examples_without_local_paths(self) -> None: + report = run_public_example_verification(ROOT) + + self.assertTrue(report["valid"]) + self.assertEqual(report["summary"], {"check_count": 11, "passed_check_count": 11}) + self.assertNotIn(str(ROOT), json.dumps(report, ensure_ascii=False)) + self.assertTrue(all(check["valid"] for check in report["checks"])) + + def test_accepts_an_explicit_checkout_root(self) -> None: + self.assertEqual(find_project_root(ROOT), ROOT) + + def test_missing_checkout_has_a_safe_error_message(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + with self.assertRaisesRegex(FileNotFoundError, "CineThread source checkout"): + find_project_root(Path(temp_dir)) + + def test_failed_verification_does_not_expose_the_checkout_path(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + temporary_root = Path(temp_dir) + (temporary_root / "pyproject.toml").write_text("", encoding="utf-8") + (temporary_root / "examples").mkdir() + + report = run_public_example_verification(temporary_root) + + self.assertFalse(report["valid"]) + self.assertNotIn(str(temporary_root), json.dumps(report, ensure_ascii=False)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_version.py b/tests/test_version.py index b0fc90a..cd8abd9 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -13,7 +13,7 @@ def test_runtime_and_project_versions_match(self) -> None: project_text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") project_version = re.search(r'^version\s*=\s*"([^"]+)"\s*$', project_text, re.MULTILINE) self.assertIsNotNone(project_version) - self.assertEqual(__version__, "0.2.0") + self.assertEqual(__version__, "0.2.1") self.assertEqual(__version__, project_version.group(1))