diff --git a/movmaker.py b/movmaker.py index ce2d9d6..5e325ea 100755 --- a/movmaker.py +++ b/movmaker.py @@ -39,11 +39,11 @@ DEFAULT_INTRO_LOGO = BASE_DIR / "img" / "moto_travel.png" INTRO_LOGO_TARGET_HEIGHT_RATIO = 0.75 INTRO_LOGO_EXTRA_HOLD = 6.0 INTRO_LOGO_FADE_OFFSET = 3.0 -INTRO_LOGO_DEFAULT_ALPHA = 1.0 -INTRO_TITLE_START_OFFSET = 4.0 +INTRO_LOGO_DEFAULT_ALPHA = 0.75 +INTRO_TITLE_START_OFFSET = 0.0 INTRO_TITLE_FADE_LENGTH = 1.0 INTRO_TITLE_VISIBLE = 6.0 -INTRO_TITLE_FADE_ENABLED = True +INTRO_TITLE_FADE_IN_ENABLED = False DEFAULT_IMAGE_DURATION = 6.0 DEFAULT_CROSSFADE_DURATION = 3.0 FIRST_IMAGE_DURATION_MULTIPLIER = 2.0 diff --git a/mvlog_worker.py b/mvlog_worker.py index 07d5196..46e9f56 100755 --- a/mvlog_worker.py +++ b/mvlog_worker.py @@ -57,7 +57,9 @@ def list_jobs(host: str, remote_root: str) -> list[str]: return [line.strip() for line in out.splitlines() if line.strip()] -def remote_file_exists(host: str, path: str) -> bool: +def remote_file_exists(host: str | None, path: str) -> bool: + if host is None: + return Path(path).is_file() return ssh(host, f"test -f {remote_q(path)}", check=False).returncode == 0 @@ -92,7 +94,13 @@ print(hashlib.sha256(json.dumps(entries, ensure_ascii=False, separators=(',', ': return ssh(host, cmd).stdout.strip() -def read_remote_json(host: str, path: str) -> dict: +def read_remote_json(host: str | None, path: str) -> dict: + if host is None: + try: + data = json.loads(Path(path).read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} res = ssh(host, f"cat {remote_q(path)} 2>/dev/null", check=False) if res.returncode != 0 or not res.stdout.strip(): return {} @@ -115,11 +123,24 @@ def write_remote_json(host: str, path: str, data: dict) -> None: ssh(host, cmd, input_text=payload) -def read_state(host: str, job_dir: str) -> dict: +def read_state(host: str | None, job_dir: str) -> dict: + if host is None: + path = Path(job_dir) / STATE_NAME + if not path.exists(): + return {} + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception: + return {} return read_remote_json(host, f"{job_dir}/{STATE_NAME}") + - -def write_state(host: str, job_dir: str, state: dict) -> None: +def write_state(host: str | None, job_dir: str, state: dict) -> None: + if host is None: + path = Path(job_dir) / STATE_NAME + path.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + path.chmod(0o664) + return path = f"{job_dir}/{STATE_NAME}" tmp_path = f"{path}.tmp.$$" data = json.dumps(state, ensure_ascii=False, indent=2) + "\n" @@ -129,7 +150,7 @@ def write_state(host: str, job_dir: str, state: dict) -> None: f"mv {remote_q(tmp_path)} {remote_q(path)}" ) ssh(host, cmd, input_text=data) - + def copy_job_from_remote(host: str, job_dir: str, local_input: Path) -> None: local_input.mkdir(parents=True, exist_ok=True) @@ -215,7 +236,23 @@ def render_job(local_input: Path, local_out: Path, extra_args: list[str]) -> Pat "--intro-logo-alpha", INTRO_LOGO_ALPHA, ] cmd = [sys.executable, str(MOVMAKER), str(local_input), "--out-dir", str(local_out), *intro_args, *extra_args] - subprocess.run(cmd, check=True) + log_dir = local_input / ".logs" + log_dir.mkdir(parents=True, exist_ok=True) + log_path = log_dir / f"mvlog_worker_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as exc: + log_path.write_text( + "Command failed:\n" + " ".join(cmd) + "\n\nSTDOUT:\n" + (exc.stdout or "") + "\n\nSTDERR:\n" + (exc.stderr or ""), + encoding="utf-8", + ) + raise RuntimeError(f"movmaker failed; see {log_path}") from exc + else: + if result.stdout or result.stderr: + log_path.write_text( + "Command:\n" + " ".join(cmd) + "\n\nSTDOUT:\n" + (result.stdout or "") + "\n\nSTDERR:\n" + (result.stderr or ""), + encoding="utf-8", + ) outputs = sorted(local_out.glob("*.mp4"), key=lambda p: p.stat().st_mtime, reverse=True) if not outputs: raise RuntimeError("movmaker finished but no MP4 was produced") @@ -338,6 +375,7 @@ def process_job(host: str, remote_root: str, job: str, work_dir: Path, extra_arg def main() -> int: parser = argparse.ArgumentParser(description="Render changed MVLog input dirs from a remote web server.") + parser.add_argument("--local", action="store_true", help="Treat remote paths as local; skip SSH") parser.add_argument("--host", default="192.168.0.204", help="SSH host for the web server") parser.add_argument("--remote-root", default="/var/www/html/mvlog", help="Remote MVLog web root") parser.add_argument("--work-dir", default=str(Path.home() / ".cache" / "mvlog-worker"), help="Local work directory") @@ -348,9 +386,9 @@ def main() -> int: work_dir = Path(args.work_dir) work_dir.mkdir(parents=True, exist_ok=True) - jobs = args.job or list_jobs(args.host, args.remote_root) + jobs = args.job or list_jobs(None if args.local else args.host, args.remote_root) for job in jobs: - process_job(args.host, args.remote_root, job, work_dir, args.movmaker_arg, args.force) + process_job(None if args.local else args.host, args.remote_root, job, work_dir, args.movmaker_arg, args.force) return 0