scripts/init_job.py
scripts/init_job.pyBrowse 9 files
1,301 tokens
5,443 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""Create a non-overwriting presenter-video job from minimal inputs."""3 4from __future__ import annotations5 6import argparse7import json8import re9import sys10from pathlib import Path11 12 13SKILL_DIR = Path(__file__).resolve().parent.parent14TEMPLATE = SKILL_DIR / "assets" / "job.template.json"15ASPECT_DEFAULTS = {16 "9:16": (1080, 1920),17 "16:9": (1920, 1080),18 "1:1": (1080, 1080),19 "4:5": (1080, 1350),20}21 22 23def absolute_existing(path_text: str, label: str) -> str:24 path = Path(path_text).expanduser().resolve()25 if not path.is_file():26 raise ValueError(f"{label} does not exist or is not a file: {path}")27 return str(path)28 29 30def slugify(value: str) -> str:31 slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")32 return slug or "presenter-video"33 34 35def parse_args() -> argparse.Namespace:36 parser = argparse.ArgumentParser(description=__doc__)37 parser.add_argument("--job-dir", required=True)38 parser.add_argument("--presenter-image", required=True)39 source = parser.add_mutually_exclusive_group(required=True)40 source.add_argument("--topic")41 source.add_argument("--script", help="Existing local script file")42 parser.add_argument("--voice-sample")43 parser.add_argument("--supporting-media", action="append", default=[])44 parser.add_argument("--language", default="auto")45 parser.add_argument("--audience", default="general")46 parser.add_argument("--duration", type=float, default=60.0)47 parser.add_argument("--aspect", choices=sorted(ASPECT_DEFAULTS), default="9:16")48 parser.add_argument("--width", type=int)49 parser.add_argument("--height", type=int)50 parser.add_argument("--fps", type=int, choices=(24, 25, 30, 50, 60), default=30)51 parser.add_argument("--style", default="credible contemporary presenter")52 parser.add_argument("--watermark", default="")53 parser.add_argument("--cta", default="")54 parser.add_argument("--rights-confirmed", action="store_true")55 parser.add_argument("--adult-presenter-confirmed", action="store_true")56 parser.add_argument("--remote-upload-approved", action="store_true")57 parser.add_argument("--voice-clone-approved", action="store_true")58 return parser.parse_args()59 60 61def main() -> int:62 args = parse_args()63 if not 5 <= args.duration <= 1800:64 raise ValueError("--duration must be between 5 and 1800 seconds")65 if (args.width is None) != (args.height is None):66 raise ValueError("--width and --height must be supplied together")67 68 width, height = ASPECT_DEFAULTS[args.aspect]69 if args.width is not None and args.height is not None:70 width, height = args.width, args.height71 if min(width, height) < 256 or max(width, height) > 7680:72 raise ValueError("custom dimensions must be between 256 and 7680 pixels")73 74 job_dir = Path(args.job_dir).expanduser().resolve()75 if job_dir.exists() and any(job_dir.iterdir()):76 raise ValueError(f"job directory must be absent or empty: {job_dir}")77 job_dir.mkdir(parents=True, exist_ok=True)78 79 manifest = json.loads(TEMPLATE.read_text(encoding="utf-8"))80 manifest["job_id"] = slugify(job_dir.name)81 manifest["input"]["topic"] = (args.topic or "").strip()82 manifest["input"]["script_path"] = (83 absolute_existing(args.script, "script") if args.script else ""84 )85 manifest["input"]["presenter_image"] = absolute_existing(86 args.presenter_image, "presenter image"87 )88 manifest["input"]["voice_sample"] = (89 absolute_existing(args.voice_sample, "voice sample")90 if args.voice_sample91 else ""92 )93 manifest["input"]["supporting_media"] = [94 absolute_existing(item, "supporting media") for item in args.supporting_media95 ]96 manifest["input"]["rights_confirmed"] = args.rights_confirmed97 manifest["input"]["adult_presenter_confirmed"] = args.adult_presenter_confirmed98 manifest["input"]["remote_upload_approved"] = args.remote_upload_approved99 manifest["input"]["voice_clone_approved"] = args.voice_clone_approved100 101 creative = manifest["creative"]102 creative.update(103 {104 "language": args.language,105 "audience": args.audience,106 "duration_target_s": args.duration,107 "aspect": args.aspect,108 "width": width,109 "height": height,110 "fps": args.fps,111 "style": args.style,112 "watermark": args.watermark,113 "cta": args.cta,114 }115 )116 117 directories = [118 "docs",119 "assets/source",120 "assets/audio/reference",121 "assets/audio/raw",122 "assets/audio/final",123 "assets/video/candidates",124 "assets/video/selected",125 "assets/video/render",126 "assets/captions",127 "qa/requests",128 "qa/asr",129 "qa/contacts",130 "qa/reports",131 "renders",132 "outputs",133 ]134 for relative in directories:135 (job_dir / relative).mkdir(parents=True, exist_ok=True)136 137 job_path = job_dir / "job.json"138 job_path.write_text(139 json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",140 encoding="utf-8",141 )142 print(json.dumps({"job": str(job_path), "state": "intake"}, ensure_ascii=False))143 return 0144 145 146if __name__ == "__main__":147 try:148 raise SystemExit(main())149 except ValueError as exc:150 print(f"ERROR: {exc}", file=sys.stderr)151 raise SystemExit(2)152