File size: 17,406 Bytes
2e1a095 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | 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()
|