| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | PPT Master - Animation Config Tool |
| 4 | |
| 5 | Create and validate optional PPTX animation and deterministic Morph sidecars. |
| 6 | |
| 7 | Usage: |
| 8 | python3 scripts/animation_config.py scaffold <project_path> |
| 9 | python3 scripts/animation_config.py list-groups <project_path> |
| 10 | python3 scripts/animation_config.py validate <project_path> |
| 11 | |
| 12 | Examples: |
| 13 | python3 scripts/animation_config.py scaffold projects/demo --force |
| 14 | python3 scripts/animation_config.py list-groups projects/demo |
| 15 | python3 scripts/animation_config.py validate projects/demo |
| 16 | |
| 17 | Dependencies: |
| 18 | None (standard library only) |
| 19 | """ |
| 20 | |
| 21 | from __future__ import annotations |
| 22 | |
| 23 | import argparse |
| 24 | import sys |
| 25 | from pathlib import Path |
| 26 | |
| 27 | _SCRIPTS_DIR = Path(__file__).resolve().parent |
| 28 | if str(_SCRIPTS_DIR) not in sys.path: |
| 29 | sys.path.insert(0, str(_SCRIPTS_DIR)) |
| 30 | |
| 31 | from console_encoding import configure_utf8_stdio # noqa: E402 |
| 32 | from svg_to_pptx.animation_config import ( # noqa: E402 |
| 33 | build_group_listing, |
| 34 | load_animation_config, |
| 35 | validate_animation_config, |
| 36 | validate_animation_config_errors, |
| 37 | validate_transition_config, |
| 38 | write_scaffold, |
| 39 | ) |
| 40 | |
| 41 | configure_utf8_stdio() |
| 42 | |
| 43 | |
| 44 | def build_parser() -> argparse.ArgumentParser: |
| 45 | parser = argparse.ArgumentParser( |
| 46 | description='Create or validate PPTX motion sidecar configuration.', |
| 47 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 48 | ) |
| 49 | subparsers = parser.add_subparsers(dest='command', required=True) |
| 50 | |
| 51 | scaffold = subparsers.add_parser( |
| 52 | 'scaffold', |
| 53 | help='scan svg_output/*.svg and write animations.json scaffold', |
| 54 | ) |
| 55 | scaffold.add_argument('project_path', help='Project directory') |
| 56 | scaffold.add_argument('-o', '--output', default=None, help='Output path; default: <project>/animations.json') |
| 57 | scaffold.add_argument('--force', action='store_true', help='Overwrite an existing output file') |
| 58 | |
| 59 | list_groups = subparsers.add_parser( |
| 60 | 'list-groups', |
| 61 | help='print one compact line per slide listing animatable group ids ' |
| 62 | '(chrome groups excluded); use during planning to avoid reading ' |
| 63 | 'the full scaffold file', |
| 64 | ) |
| 65 | list_groups.add_argument('project_path', help='Project directory') |
| 66 | |
| 67 | validate = subparsers.add_parser( |
| 68 | 'validate', |
| 69 | help='validate animations.json values and project-local references', |
| 70 | ) |
| 71 | validate.add_argument('project_path', help='Project directory') |
| 72 | validate.add_argument('-c', '--config', default=None, help='Config path; default: <project>/animations.json') |
| 73 | |
| 74 | return parser |
| 75 | |
| 76 | |
| 77 | def main(argv: list[str] | None = None) -> int: |
| 78 | parser = build_parser() |
| 79 | args = parser.parse_args(argv) |
| 80 | project_path = Path(args.project_path) |
| 81 | if not project_path.exists(): |
| 82 | print(f'Error: Project path does not exist: {project_path}', file=sys.stderr) |
| 83 | return 1 |
| 84 | |
| 85 | if args.command == 'scaffold': |
| 86 | try: |
| 87 | output_path = write_scaffold( |
| 88 | project_path, |
| 89 | output_path=args.output, |
| 90 | force=args.force, |
| 91 | ) |
| 92 | except (FileExistsError, ValueError) as exc: |
| 93 | print(f'Error: {exc}', file=sys.stderr) |
| 94 | if isinstance(exc, FileExistsError): |
| 95 | print('Use --force to overwrite.', file=sys.stderr) |
| 96 | return 1 |
| 97 | print(f'Animation config scaffold written: {output_path}') |
| 98 | return 0 |
| 99 | |
| 100 | if args.command == 'list-groups': |
| 101 | try: |
| 102 | lines, anonymous = build_group_listing(project_path) |
| 103 | except ValueError as exc: |
| 104 | print(f'Error: {exc}', file=sys.stderr) |
| 105 | return 1 |
| 106 | for line in lines: |
| 107 | print(line) |
| 108 | for warning in anonymous: |
| 109 | print(f'Warning: {warning}', file=sys.stderr) |
| 110 | return 0 |
| 111 | |
| 112 | if args.command == 'validate': |
| 113 | try: |
| 114 | config = load_animation_config(project_path, args.config) |
| 115 | except Exception as exc: |
| 116 | print(f'Error: {exc}', file=sys.stderr) |
| 117 | return 1 |
| 118 | if config is None: |
| 119 | print('No animations.json found; default animation policy will be used.') |
| 120 | return 0 |
| 121 | errors = list(dict.fromkeys( |
| 122 | validate_transition_config(config) |
| 123 | + validate_animation_config_errors(config) |
| 124 | )) |
| 125 | if errors: |
| 126 | for error in errors: |
| 127 | print(f'Error: {error}', file=sys.stderr) |
| 128 | return 1 |
| 129 | reference_messages = validate_animation_config(project_path, config) |
| 130 | reference_warnings = [ |
| 131 | message for message in reference_messages |
| 132 | if ' has no id and cannot be customized in animations.json' in message |
| 133 | ] |
| 134 | reference_errors = [ |
| 135 | message for message in reference_messages |
| 136 | if message not in reference_warnings |
| 137 | ] |
| 138 | for warning in reference_warnings: |
| 139 | print(f'Warning: {warning}', file=sys.stderr) |
| 140 | if reference_errors: |
| 141 | for error in reference_errors: |
| 142 | print(f'Error: {error}', file=sys.stderr) |
| 143 | return 1 |
| 144 | print('Animation config validated successfully.') |
| 145 | return 0 |
| 146 | |
| 147 | parser.print_help() |
| 148 | return 1 |
| 149 | |
| 150 | |
| 151 | if __name__ == '__main__': |
| 152 | raise SystemExit(main()) |
| 153 |