from __future__ import annotations import argparse import json import sys from dataclasses import asdict from pathlib import Path ROOT_DIR = Path(__file__).resolve().parent.parent if str(ROOT_DIR) not in sys.path: sys.path.insert(0, str(ROOT_DIR)) from scripts.deployment_handoff import DEFAULT_OUT as DEFAULT_HANDOFF_OUT from scripts.deployment_handoff import format_key_value_block from scripts.deployment_handoff import build_handoff, write_markdown from scripts.deployment_status import build_status from scripts.export_hf_space import DEFAULT_OUTPUT as DEFAULT_SPACE_OUT from scripts.export_hf_space import export_hf_space, validate_export from scripts.next_deployment_step import choose_next_step from scripts.validate_deployment_env import summarize as summarize_env_checks from scripts.validate_deployment_env import validate_deployment_env DEFAULT_REPORT = ROOT_DIR / "outputs" / "deployment-prep.json" DEFAULT_QUICKSTART = ROOT_DIR / "outputs" / "deployment-quickstart.md" DEFAULT_VERCEL_ENV_OUT = ROOT_DIR / "outputs" / "vercel-production.env" DEFAULT_WORKER_ENV_OUT = ROOT_DIR / "outputs" / "worker-secrets.env" DEFAULT_VERCEL_COMMANDS_OUT = ROOT_DIR / "outputs" / "apply-vercel-env.ps1" DEFAULT_WORKER_SETTINGS_OUT = ROOT_DIR / "outputs" / "worker-space-settings.md" def prepare_live_deployment( worker_url: str | None = None, origin: str | None = None, code: str = "1234", secret_key: str | None = None, *, space_out: Path = DEFAULT_SPACE_OUT, report_out: Path = DEFAULT_REPORT, handoff_out: Path = DEFAULT_HANDOFF_OUT, quickstart_out: Path = DEFAULT_QUICKSTART, vercel_env_out: Path = DEFAULT_VERCEL_ENV_OUT, worker_env_out: Path = DEFAULT_WORKER_ENV_OUT, vercel_commands_out: Path = DEFAULT_VERCEL_COMMANDS_OUT, worker_settings_out: Path = DEFAULT_WORKER_SETTINGS_OUT, force: bool = True, ) -> dict[str, object]: export_result = export_hf_space(space_out, force=force) export_issues = validate_export(space_out) handoff_path: str | None = None verify_command: str | None = None vercel_env_path: str | None = None worker_env_path: str | None = None vercel_commands_path: str | None = None worker_settings_path: str | None = None env_validation: dict[str, object] | None = None if worker_url and origin: handoff = build_handoff(worker_url, origin, code, secret_key) write_markdown(handoff_out, handoff) write_env_file(vercel_env_out, handoff.vercel_env) write_env_file(worker_env_out, handoff.worker_secrets) write_vercel_commands( vercel_commands_out, handoff.vercel_cleanup_commands, handoff.vercel_cli_commands, validate_command=handoff.commands["validateEnv"], preflight_command=handoff.commands["diagnoseVercelWorker"], ) write_worker_settings(worker_settings_out, handoff) handoff_path = str(handoff_out) vercel_env_path = str(vercel_env_out) worker_env_path = str(worker_env_out) vercel_commands_path = str(vercel_commands_out) worker_settings_path = str(worker_settings_out) verify_command = handoff.commands["verifyLive"] env_validation = summarize_env_checks( validate_deployment_env( handoff.vercel_env, handoff.worker_secrets, expected_worker_url=handoff.worker_url, expected_origin=handoff.vercel_origin, ) ) next_step = choose_next_step(worker_url=worker_url, vercel_origin=origin, code=code) status = build_status(worker_url=worker_url, vercel_origin=origin, code=code) write_quickstart( quickstart_out, space_out=space_out, handoff_out=Path(handoff_path) if handoff_path else handoff_out, worker_url=worker_url, origin=origin, code=code, worker_settings_out=Path(worker_settings_path) if worker_settings_path else worker_settings_out, next_title=next_step.title, next_detail=next_step.detail, next_command=next_step.command, ) report: dict[str, object] = { "ready": not export_issues, "spaceBundle": export_result, "spaceIssues": export_issues, "handoff": handoff_path, "vercelEnv": vercel_env_path, "workerEnv": worker_env_path, "vercelCommands": vercel_commands_path, "workerSettings": worker_settings_path, "envValidation": env_validation, "quickstart": str(quickstart_out), "verifyCommand": verify_command, "nextStep": asdict(next_step), "deploymentStatus": asdict(status), } report_out.parent.mkdir(parents=True, exist_ok=True) report_out.write_text(json.dumps(report, indent=2), encoding="utf-8") return report def write_env_file(path: Path, values: dict[str, str]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(format_key_value_block(values) + "\n", encoding="utf-8") def write_vercel_commands( path: Path, cleanup_commands: list[str], env_commands: list[str], *, validate_command: str | None = None, preflight_command: str | None = None, ) -> None: path.parent.mkdir(parents=True, exist_ok=True) lines = [ "# Generated by scripts\\prepare_live_deployment.py.", "# This file contains deployment secrets. Keep it private and do not commit it.", "# Production Vercel audio must use WORKER_BASE_URL; direct Hugging Face cloud TTS is only for short tests.", "", "# Remove temporary direct-cloud fallback values from production.", *cleanup_commands, "", "# Add/update the production variables for the Vercel shell.", *env_commands, "", "# Verify that Vercel points to the worker and does not have direct-cloud fallback variables enabled.", *(["# " + validate_command] if validate_command else []), "", "# After Vercel redeploys and the worker is awake, verify browser-to-worker reachability and CORS.", *(["# " + preflight_command] if preflight_command else []), "", ] path.write_text("\n".join(lines), encoding="utf-8") def write_worker_settings(path: Path, handoff) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text( "\n".join( [ "# Hugging Face Worker Settings", "", "Generated by `scripts\\prepare_live_deployment.py`. This file contains deployment secrets, so keep it private and do not commit it.", "", "## Space Secrets", "", "Paste these into the Hugging Face Space **Settings > Secrets** page:", "", "```text", format_key_value_block(handoff.worker_secrets), "```", "", "## Balanced Docker Build Args", "", "Use these first for a strong Arabic OCR worker without immediately enabling the heaviest QARI/PaddleOCR-VL paths:", "", "```text", format_key_value_block(handoff.recommended_build_args), "```", "", "## Maximum Quality Docker Build Args", "", "Use these only on larger hardware after a 5-page benchmark shows the heavier models help this book:", "", "```text", format_key_value_block(handoff.maximum_quality_build_args), "```", "", "## Verify The Settings", "", "After setting the Space secrets/build args, rebuild or restart the Space, then validate the generated env files:", "", "```powershell", handoff.commands["validateEnv"], "```", "", "Then run hosted preflight before any real PDF upload:", "", "```powershell", handoff.commands["diagnoseVercelWorker"], "```", "", "Then run the live proof:", "", "```powershell", handoff.commands["verifyLive"], "```", "", ] ), encoding="utf-8", ) def write_quickstart( path: Path, *, space_out: Path, handoff_out: Path, worker_url: str | None, origin: str | None, code: str, worker_settings_out: Path, next_title: str, next_detail: str, next_command: str, ) -> None: path.parent.mkdir(parents=True, exist_ok=True) worker_display = worker_url or "https://your-space.hf.space" origin_display = origin or "https://your-vercel-app.vercel.app" handoff_command = ( f"python scripts\\deployment_handoff.py {worker_display} " f"--origin {origin_display} --code {code}" ) hosted_preflight_command = ( f"python scripts\\hosted_preflight.py {origin_display} " f"--code {code} --worker-url {worker_display}" ) prepare_command = ( f"python scripts\\prepare_live_deployment.py --worker-url {worker_display} " f"--origin {origin_display} --code {code}" ) path.write_text( "\n".join( [ "# Arabic Audio Reader Deployment Quickstart", "", "This file is generated by `scripts\\prepare_live_deployment.py` so the live deployment steps stay in one place.", "", "## What This Prepared", "", f"- Hugging Face Docker Space bundle: `{space_out}`", f"- Deployment handoff, after real URLs exist: `{handoff_out}`", f"- Hugging Face worker settings sheet, after real URLs exist: `{worker_settings_out}`", f"- Access code: `{code}`", "", "## Create The Free Worker", "", "Open: https://huggingface.co/new-space", "Reference: https://huggingface.co/docs/hub/main/en/spaces-sdks-docker", "", "1. Create a Hugging Face Space.", "2. Choose **Docker** as the Space SDK.", f"3. Upload or push the generated folder: `{space_out}`", "4. Use the secrets/build args from the generated worker settings sheet after you know the real Vercel URL.", "", "Free CPU Spaces are enough for proof and small samples. For a full 100 MB+ scanned book, expect slow jobs and cold starts unless you move the worker to stronger hardware.", "", "## Create The Vercel Site", "", "Open: https://vercel.com/new", "Reference: https://vercel.com/docs/frameworks/backend/fastapi", "", "1. Import this GitHub repo into Vercel.", "2. Use the default/Other preset; the repo already has `vercel.json` for the Python FastAPI entrypoint.", "3. Set `ACCESS_CODE`, `SECRET_KEY`, and `WORKER_BASE_URL` from the deployment handoff.", "4. Remove any temporary direct-cloud TTS variables from earlier tests: `ENABLE_DIRECT_CLOUD_TTS`, `HF_API_TOKEN`, `HF_TTS_MODEL`, and `DEFAULT_VOICE_ID`.", "5. Deploy to Production.", "6. If generated env files exist, run `scripts\\validate_deployment_env.py` before the final deploy.", "7. If `outputs\\apply-vercel-env.ps1` exists, it contains the generated Vercel env commands.", "", "## After Both URLs Exist", "", "Regenerate the exact secret/env/verification sheet with the real URLs:", "", "```powershell", handoff_command, "```", "", "Or refresh the worker bundle and handoff together:", "", "```powershell", prepare_command, "```", "", "Before uploading a large PDF, run hosted preflight from Vercel to the worker:", "", "```powershell", hosted_preflight_command, "```", "", "It writes `outputs\\hosted-preflight.json` and must pass `site worker reachable from vercel` and `site worker CORS ready`.", "", "## Safety Checklist", "", "- Validate generated env files with `python scripts\\validate_deployment_env.py --vercel-env outputs\\vercel-production.env --worker-env outputs\\worker-secrets.env --worker-url " f"{worker_display} --origin {origin_display}`.", "- Put the same generated `SECRET_KEY` in Hugging Face and Vercel.", "- Put the same access code in Hugging Face and Vercel.", "- Point Hugging Face `CORS_ORIGINS` at the exact Vercel production URL.", "- Point Vercel `WORKER_BASE_URL` at the exact Hugging Face Space URL.", "- Keep Vercel direct Hugging Face TTS disabled in production; downloadable audio should come from the worker.", "- Run the live proof command before giving the site to anyone.", "", "## Current Next Command", "", f"**{next_title}**", "", next_detail, "", "```powershell", next_command, "```", "", "The project is not considered live-finished until `outputs\\live-deployment-proof.json` says `complete: true` and the final audit has no live-deployment warnings.", "", ] ), encoding="utf-8", ) def main() -> None: parser = argparse.ArgumentParser(description="Prepare the live Vercel plus worker deployment artifacts.") parser.add_argument("--worker-url", help="Live worker URL, for example https://your-space.hf.space") parser.add_argument("--origin", help="Live Vercel origin, for example https://your-app.vercel.app") parser.add_argument("--code", default="1234", help="Access code for the site and worker.") parser.add_argument("--secret-key", help="Optional fixed cookie-signing secret. Omit to generate one in the handoff.") parser.add_argument("--space-out", type=Path, default=DEFAULT_SPACE_OUT, help="Destination for the Hugging Face Space bundle.") parser.add_argument("--report-out", type=Path, default=DEFAULT_REPORT, help="JSON deployment prep report path.") parser.add_argument("--handoff-out", type=Path, default=DEFAULT_HANDOFF_OUT, help="Markdown handoff path when URLs are provided.") parser.add_argument("--quickstart-out", type=Path, default=DEFAULT_QUICKSTART, help="Markdown quickstart path.") parser.add_argument("--vercel-env-out", type=Path, default=DEFAULT_VERCEL_ENV_OUT, help="Generated .env-style Vercel production variables.") parser.add_argument("--worker-env-out", type=Path, default=DEFAULT_WORKER_ENV_OUT, help="Generated .env-style worker/Space secrets.") parser.add_argument("--vercel-commands-out", type=Path, default=DEFAULT_VERCEL_COMMANDS_OUT, help="Generated private PowerShell commands for Vercel production env.") parser.add_argument("--worker-settings-out", type=Path, default=DEFAULT_WORKER_SETTINGS_OUT, help="Generated private Hugging Face worker settings sheet.") parser.add_argument("--no-force", action="store_true", help="Do not replace an existing Space bundle.") parser.add_argument("--json", action="store_true", help="Print JSON instead of a compact summary.") args = parser.parse_args() report = prepare_live_deployment( args.worker_url, args.origin, args.code, args.secret_key, space_out=args.space_out, report_out=args.report_out, handoff_out=args.handoff_out, quickstart_out=args.quickstart_out, vercel_env_out=args.vercel_env_out, worker_env_out=args.worker_env_out, vercel_commands_out=args.vercel_commands_out, worker_settings_out=args.worker_settings_out, force=not args.no_force, ) if args.json: print(json.dumps(report, indent=2)) else: print(f"Wrote deployment prep report to {args.report_out}") print(f"Exported Hugging Face Space bundle to {report['spaceBundle']['outputDir']}") print(f"Wrote deployment quickstart to {report['quickstart']}") if report["handoff"]: print(f"Wrote deployment handoff to {report['handoff']}") print(f"Wrote Vercel env file to {report['vercelEnv']}") print(f"Wrote worker env file to {report['workerEnv']}") print(f"Wrote Vercel command file to {report['vercelCommands']}") print(f"Wrote worker settings sheet to {report['workerSettings']}") print(report["nextStep"]["command"]) if report["spaceIssues"]: raise SystemExit(1) if __name__ == "__main__": main()