228 lines
8.3 KiB
Python
228 lines
8.3 KiB
Python
"""Command-line interface: analyze / scaffold / check-plan / apply / validate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
_SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
|
if str(_SCRIPTS_DIR) not in sys.path:
|
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
|
|
from attribution_guard import require_skill_integrity # noqa: E402
|
|
from console_encoding import configure_utf8_stdio # noqa: E402
|
|
from pptx_transitions import ( # noqa: E402
|
|
LEGACY_TRANSITION_KEYS,
|
|
NATIVE_TRANSITION_KEYS,
|
|
)
|
|
|
|
configure_utf8_stdio()
|
|
|
|
if __package__ in {None, ''}:
|
|
import types
|
|
|
|
package_dir = Path(__file__).resolve().parent
|
|
while str(package_dir) in sys.path:
|
|
sys.path.remove(str(package_dir))
|
|
scripts_dir = Path(__file__).resolve().parents[1]
|
|
if str(scripts_dir) not in sys.path:
|
|
sys.path.insert(0, str(scripts_dir))
|
|
package = types.ModuleType("template_fill_pptx")
|
|
package.__path__ = [str(package_dir)] # type: ignore[attr-defined]
|
|
sys.modules.setdefault("template_fill_pptx", package)
|
|
__package__ = "template_fill_pptx"
|
|
|
|
from .analyzer import analyze_pptx
|
|
from .applier import apply_plan
|
|
from .checker import check_plan, print_check_report
|
|
from .ooxml import _load_json, _write_json
|
|
from .scaffolder import scaffold_plan
|
|
from .transitions import (
|
|
DEFAULT_TRANSITION,
|
|
DEFAULT_TRANSITION_DURATION,
|
|
KEEP_TRANSITION,
|
|
)
|
|
from .validator import print_validate_report, validate_project
|
|
|
|
|
|
def _parse_slide_list(value: str | None) -> list[int] | None:
|
|
if not value:
|
|
return None
|
|
slides: list[int] = []
|
|
for part in value.split(","):
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
if "-" in part:
|
|
start, end = part.split("-", 1)
|
|
slides.extend(range(int(start), int(end) + 1))
|
|
else:
|
|
slides.append(int(part))
|
|
return slides
|
|
|
|
|
|
def _timestamped_pptx_path(path: Path) -> Path:
|
|
if path.suffix.lower() != ".pptx":
|
|
return path
|
|
if re.search(r"_\d{8}_\d{6}$", path.stem):
|
|
return path
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
return path.with_name(f"{path.stem}_{timestamp}{path.suffix}")
|
|
|
|
|
|
def _plan_confirmed(plan: dict) -> bool:
|
|
return plan.get("status") == "confirmed"
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description="Analyze and fill native PPTX templates without converting slides to SVG.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
analyze = subparsers.add_parser("analyze", help="Extract slide library JSON from a PPTX")
|
|
analyze.add_argument("pptx_file", help="Source PPTX file")
|
|
analyze.add_argument("-o", "--output", required=True, help="Output <stem>.slide_library.json path")
|
|
|
|
scaffold = subparsers.add_parser("scaffold", help="Create an editable fill plan skeleton")
|
|
scaffold.add_argument("library_json", help="<stem>.slide_library.json from analyze")
|
|
scaffold.add_argument("-o", "--output", required=True, help="Output fill_plan.json path")
|
|
scaffold.add_argument(
|
|
"--slides",
|
|
help="Comma/range source slide list, e.g. 1,3,5-7. Defaults to first six slides.",
|
|
)
|
|
scaffold.add_argument(
|
|
"--include-empty",
|
|
action="store_true",
|
|
help="Include empty text slots in the scaffold. Defaults to text-bearing slots only.",
|
|
)
|
|
|
|
check = subparsers.add_parser("check-plan", help="Check a fill plan against source slot capacity")
|
|
check.add_argument("library_json", help="<stem>.slide_library.json from analyze")
|
|
check.add_argument("plan_json", help="Fill plan JSON")
|
|
check.add_argument("-o", "--output", help="Optional JSON report output path")
|
|
|
|
apply = subparsers.add_parser("apply", help="Apply fill plan and write a new PPTX")
|
|
apply.add_argument("pptx_file", help="Source PPTX file")
|
|
apply.add_argument("plan_json", help="Fill plan JSON")
|
|
apply.add_argument(
|
|
"-o",
|
|
"--output",
|
|
required=True,
|
|
help=(
|
|
"Output PPTX path. A _YYYYMMDD_HHMMSS timestamp is appended "
|
|
"automatically unless the stem already ends with one."
|
|
),
|
|
)
|
|
apply.add_argument(
|
|
"--transition",
|
|
choices=[
|
|
*NATIVE_TRANSITION_KEYS,
|
|
*LEGACY_TRANSITION_KEYS,
|
|
"none",
|
|
KEEP_TRANSITION,
|
|
],
|
|
default=DEFAULT_TRANSITION,
|
|
help=(
|
|
"Page-to-page transition policy for every cloned slide "
|
|
"(per-slide 'transition' in the plan overrides this). "
|
|
"Use a PowerPoint-native key; old names are compatibility inputs. "
|
|
f"Default: {DEFAULT_TRANSITION} (preserve the source). "
|
|
"Use 'none' to remove visual motion."
|
|
),
|
|
)
|
|
apply.add_argument(
|
|
"--transition-duration",
|
|
type=float,
|
|
default=DEFAULT_TRANSITION_DURATION,
|
|
help="Transition duration in seconds (default: 0.5).",
|
|
)
|
|
apply.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="apply without a confirmed fill plan (deliberate recovery/debug only)",
|
|
)
|
|
|
|
validate = subparsers.add_parser("validate", help="Read back and validate the latest project export")
|
|
validate.add_argument("project_path", help="Template-fill project directory")
|
|
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
require_skill_integrity()
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
if args.command == "analyze":
|
|
pptx_path = Path(args.pptx_file).expanduser().resolve()
|
|
if not pptx_path.exists():
|
|
print(f"Error: file does not exist: {pptx_path}", file=sys.stderr)
|
|
return 1
|
|
library = analyze_pptx(pptx_path)
|
|
_write_json(Path(args.output).expanduser().resolve(), library)
|
|
print(f"Analyzed {library['slide_count']} slides -> {args.output}", file=sys.stderr)
|
|
return 0
|
|
|
|
if args.command == "scaffold":
|
|
library = _load_json(Path(args.library_json).expanduser().resolve())
|
|
plan = scaffold_plan(
|
|
library,
|
|
_parse_slide_list(args.slides),
|
|
include_empty=args.include_empty,
|
|
)
|
|
_write_json(Path(args.output).expanduser().resolve(), plan)
|
|
print(f"Plan scaffold -> {args.output}", file=sys.stderr)
|
|
return 0
|
|
|
|
if args.command == "check-plan":
|
|
library = _load_json(Path(args.library_json).expanduser().resolve())
|
|
plan = _load_json(Path(args.plan_json).expanduser().resolve())
|
|
report = check_plan(library, plan)
|
|
print_check_report(report)
|
|
if args.output:
|
|
_write_json(Path(args.output).expanduser().resolve(), report)
|
|
print(f"Check report -> {args.output}", file=sys.stderr)
|
|
return 0 if report["summary"]["error"] == 0 else 1
|
|
|
|
if args.command == "apply":
|
|
pptx_path = Path(args.pptx_file).expanduser().resolve()
|
|
plan = _load_json(Path(args.plan_json).expanduser().resolve())
|
|
if not _plan_confirmed(plan) and not args.force:
|
|
print(
|
|
"Error: fill plan is not confirmed: "
|
|
f"{Path(args.plan_json).expanduser().resolve()} "
|
|
'(set status to "confirmed" after user approval, or pass --force)',
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
output_path = _timestamped_pptx_path(Path(args.output).expanduser().resolve())
|
|
apply_plan(
|
|
pptx_path,
|
|
plan,
|
|
output_path,
|
|
transition=args.transition,
|
|
transition_duration=args.transition_duration,
|
|
)
|
|
print(f"Template-filled PPTX -> {output_path}", file=sys.stderr)
|
|
return 0
|
|
|
|
if args.command == "validate":
|
|
report = validate_project(Path(args.project_path))
|
|
print_validate_report(report)
|
|
return 0 if report["summary"]["error"] == 0 else 1
|
|
except RuntimeError as exc:
|
|
print(f"Error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
parser.print_help()
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|