返回 ppt-master
1 """Command-line interface: analyze / scaffold / check-plan / apply / validate."""
2
3 from __future__ import annotations
4
5 import argparse
6 import re
7 import sys
8 from datetime import datetime
9 from pathlib import Path
10
11 _SCRIPTS_DIR = Path(__file__).resolve().parents[1]
12 if str(_SCRIPTS_DIR) not in sys.path:
13 sys.path.insert(0, str(_SCRIPTS_DIR))
14
15 from attribution_guard import require_skill_integrity # noqa: E402
16 from console_encoding import configure_utf8_stdio # noqa: E402
17 from pptx_transitions import ( # noqa: E402
18 LEGACY_TRANSITION_KEYS,
19 NATIVE_TRANSITION_KEYS,
20 )
21
22 configure_utf8_stdio()
23
24 if __package__ in {None, ''}:
25 import types
26
27 package_dir = Path(__file__).resolve().parent
28 while str(package_dir) in sys.path:
29 sys.path.remove(str(package_dir))
30 scripts_dir = Path(__file__).resolve().parents[1]
31 if str(scripts_dir) not in sys.path:
32 sys.path.insert(0, str(scripts_dir))
33 package = types.ModuleType("template_fill_pptx")
34 package.__path__ = [str(package_dir)] # type: ignore[attr-defined]
35 sys.modules.setdefault("template_fill_pptx", package)
36 __package__ = "template_fill_pptx"
37
38 from .analyzer import analyze_pptx
39 from .applier import apply_plan
40 from .checker import check_plan, print_check_report
41 from .ooxml import _load_json, _write_json
42 from .scaffolder import scaffold_plan
43 from .transitions import (
44 DEFAULT_TRANSITION,
45 DEFAULT_TRANSITION_DURATION,
46 KEEP_TRANSITION,
47 )
48 from .validator import print_validate_report, validate_project
49
50
51 def _parse_slide_list(value: str | None) -> list[int] | None:
52 if not value:
53 return None
54 slides: list[int] = []
55 for part in value.split(","):
56 part = part.strip()
57 if not part:
58 continue
59 if "-" in part:
60 start, end = part.split("-", 1)
61 slides.extend(range(int(start), int(end) + 1))
62 else:
63 slides.append(int(part))
64 return slides
65
66
67 def _timestamped_pptx_path(path: Path) -> Path:
68 if path.suffix.lower() != ".pptx":
69 return path
70 if re.search(r"_\d{8}_\d{6}$", path.stem):
71 return path
72 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
73 return path.with_name(f"{path.stem}_{timestamp}{path.suffix}")
74
75
76 def _plan_confirmed(plan: dict) -> bool:
77 return plan.get("status") == "confirmed"
78
79
80 def build_parser() -> argparse.ArgumentParser:
81 parser = argparse.ArgumentParser(
82 description="Analyze and fill native PPTX templates without converting slides to SVG.",
83 formatter_class=argparse.RawDescriptionHelpFormatter,
84 )
85 subparsers = parser.add_subparsers(dest="command", required=True)
86
87 analyze = subparsers.add_parser("analyze", help="Extract slide library JSON from a PPTX")
88 analyze.add_argument("pptx_file", help="Source PPTX file")
89 analyze.add_argument("-o", "--output", required=True, help="Output <stem>.slide_library.json path")
90
91 scaffold = subparsers.add_parser("scaffold", help="Create an editable fill plan skeleton")
92 scaffold.add_argument("library_json", help="<stem>.slide_library.json from analyze")
93 scaffold.add_argument("-o", "--output", required=True, help="Output fill_plan.json path")
94 scaffold.add_argument(
95 "--slides",
96 help="Comma/range source slide list, e.g. 1,3,5-7. Defaults to first six slides.",
97 )
98 scaffold.add_argument(
99 "--include-empty",
100 action="store_true",
101 help="Include empty text slots in the scaffold. Defaults to text-bearing slots only.",
102 )
103
104 check = subparsers.add_parser("check-plan", help="Check a fill plan against source slot capacity")
105 check.add_argument("library_json", help="<stem>.slide_library.json from analyze")
106 check.add_argument("plan_json", help="Fill plan JSON")
107 check.add_argument("-o", "--output", help="Optional JSON report output path")
108
109 apply = subparsers.add_parser("apply", help="Apply fill plan and write a new PPTX")
110 apply.add_argument("pptx_file", help="Source PPTX file")
111 apply.add_argument("plan_json", help="Fill plan JSON")
112 apply.add_argument(
113 "-o",
114 "--output",
115 required=True,
116 help=(
117 "Output PPTX path. A _YYYYMMDD_HHMMSS timestamp is appended "
118 "automatically unless the stem already ends with one."
119 ),
120 )
121 apply.add_argument(
122 "--transition",
123 choices=[
124 *NATIVE_TRANSITION_KEYS,
125 *LEGACY_TRANSITION_KEYS,
126 "none",
127 KEEP_TRANSITION,
128 ],
129 default=DEFAULT_TRANSITION,
130 help=(
131 "Page-to-page transition policy for every cloned slide "
132 "(per-slide 'transition' in the plan overrides this). "
133 "Use a PowerPoint-native key; old names are compatibility inputs. "
134 f"Default: {DEFAULT_TRANSITION} (preserve the source). "
135 "Use 'none' to remove visual motion."
136 ),
137 )
138 apply.add_argument(
139 "--transition-duration",
140 type=float,
141 default=DEFAULT_TRANSITION_DURATION,
142 help="Transition duration in seconds (default: 0.5).",
143 )
144 apply.add_argument(
145 "--force",
146 action="store_true",
147 help="apply without a confirmed fill plan (deliberate recovery/debug only)",
148 )
149
150 validate = subparsers.add_parser("validate", help="Read back and validate the latest project export")
151 validate.add_argument("project_path", help="Template-fill project directory")
152
153 return parser
154
155
156 def main(argv: list[str] | None = None) -> int:
157 require_skill_integrity()
158 parser = build_parser()
159 args = parser.parse_args(argv)
160 try:
161 if args.command == "analyze":
162 pptx_path = Path(args.pptx_file).expanduser().resolve()
163 if not pptx_path.exists():
164 print(f"Error: file does not exist: {pptx_path}", file=sys.stderr)
165 return 1
166 library = analyze_pptx(pptx_path)
167 _write_json(Path(args.output).expanduser().resolve(), library)
168 print(f"Analyzed {library['slide_count']} slides -> {args.output}", file=sys.stderr)
169 return 0
170
171 if args.command == "scaffold":
172 library = _load_json(Path(args.library_json).expanduser().resolve())
173 plan = scaffold_plan(
174 library,
175 _parse_slide_list(args.slides),
176 include_empty=args.include_empty,
177 )
178 _write_json(Path(args.output).expanduser().resolve(), plan)
179 print(f"Plan scaffold -> {args.output}", file=sys.stderr)
180 return 0
181
182 if args.command == "check-plan":
183 library = _load_json(Path(args.library_json).expanduser().resolve())
184 plan = _load_json(Path(args.plan_json).expanduser().resolve())
185 report = check_plan(library, plan)
186 print_check_report(report)
187 if args.output:
188 _write_json(Path(args.output).expanduser().resolve(), report)
189 print(f"Check report -> {args.output}", file=sys.stderr)
190 return 0 if report["summary"]["error"] == 0 else 1
191
192 if args.command == "apply":
193 pptx_path = Path(args.pptx_file).expanduser().resolve()
194 plan = _load_json(Path(args.plan_json).expanduser().resolve())
195 if not _plan_confirmed(plan) and not args.force:
196 print(
197 "Error: fill plan is not confirmed: "
198 f"{Path(args.plan_json).expanduser().resolve()} "
199 '(set status to "confirmed" after user approval, or pass --force)',
200 file=sys.stderr,
201 )
202 return 1
203 output_path = _timestamped_pptx_path(Path(args.output).expanduser().resolve())
204 apply_plan(
205 pptx_path,
206 plan,
207 output_path,
208 transition=args.transition,
209 transition_duration=args.transition_duration,
210 )
211 print(f"Template-filled PPTX -> {output_path}", file=sys.stderr)
212 return 0
213
214 if args.command == "validate":
215 report = validate_project(Path(args.project_path))
216 print_validate_report(report)
217 return 0 if report["summary"]["error"] == 0 else 1
218 except RuntimeError as exc:
219 print(f"Error: {exc}", file=sys.stderr)
220 return 1
221
222 parser.print_help()
223 return 1
224
225
226 if __name__ == "__main__":
227 raise SystemExit(main())
228
228 lines PYTHON