art-from-the-machine
Add Docker server deployment
fa3e856
Raw
History Blame Contribute Delete
5.57 kB
"""
Submit a generation job to a LingBot-World server.
Sends a POST to /generate, then polls /status/<id> until the video is ready.
Usage:
python caller.py --url http://localhost:8080 \\
--image examples/00/image.jpg \\
--prompt "A woman walks forward slowly" \\
--action_dir examples/00/
# Through a RunPod proxy:
python caller.py --url https://YOUR-POD-8080.proxy.runpod.net \\
--image photo.jpg --prompt "..."
"""
import argparse
import base64
import json
import sys
import time
from pathlib import Path
import requests
def encode_file_b64(path: str) -> str:
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def build_payload(args) -> dict:
payload = {
"image": encode_file_b64(args.image),
"prompt": args.prompt,
"frame_num": args.frame_num,
"size": args.size,
"seed": args.seed,
"guide_scale": args.guide_scale,
"sampling_steps": args.sampling_steps,
}
if args.action_dir:
action_dir = Path(args.action_dir)
poses_path = action_dir / "poses.npy"
intrinsics_path = action_dir / "intrinsics.npy"
if not poses_path.exists() or not intrinsics_path.exists():
print(f"ERROR: Expected poses.npy and intrinsics.npy in {action_dir}")
sys.exit(1)
payload["action_poses"] = encode_file_b64(str(poses_path))
payload["action_intrinsics"] = encode_file_b64(str(intrinsics_path))
print(f"Camera actions loaded from {action_dir}")
return payload
def submit(url: str, payload: dict, poll_interval: int = 10, output_path: str | None = None):
"""POST the job, then poll until completion."""
submit_url = f"{url.rstrip('/')}/generate"
print(f"Submitting to {submit_url} ...")
resp = requests.post(submit_url, json=payload, timeout=30)
if resp.status_code not in (200, 202):
print(f"ERROR {resp.status_code}: {resp.text[:500]}")
sys.exit(1)
result = resp.json()
if "error" in result:
print(f"ERROR: {result['error']}")
sys.exit(1)
job_id = result.get("id")
print(f"Job accepted — ID: {job_id}")
print("Polling for completion ...")
status_url = f"{url.rstrip('/')}/status/{job_id}"
while True:
time.sleep(poll_interval)
try:
resp = requests.get(status_url, timeout=15)
data = resp.json()
except Exception as e:
print(f" Poll error (will retry): {e}")
continue
status = data.get("status")
print(f" Status: {status}")
if status == "COMPLETED":
output = data.get("output", {})
print(f"\nDone!")
if output.get("video_url"):
print(f" Video URL : {output['video_url']}")
if output.get("video_path"):
print(f" Video path: {output['video_path']}")
# Auto-download the video from the server
download_url = f"{url.rstrip('/')}/download/{job_id}"
dest = output_path or f"{job_id}.mp4"
print(f" Downloading to {dest} ...")
try:
dl = requests.get(download_url, stream=True, timeout=300)
dl.raise_for_status()
with open(dest, "wb") as f:
for chunk in dl.iter_content(chunk_size=1024 * 1024):
f.write(chunk)
print(f" Saved to {dest}")
except Exception as e:
print(f" Download failed: {e}")
print(f" Seed : {output.get('seed')}")
print(f" Duration : {output.get('duration_sec', 0):.1f}s")
return
elif status == "FAILED":
output = data.get("output", {})
print(f"\nJob failed: {output.get('error', 'Unknown error')}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Submit a video generation job to a LingBot-World server"
)
parser.add_argument("--url", required=True,
help="Server URL (e.g. http://localhost:8080 or https://xyz-8080.proxy.runpod.net)")
parser.add_argument("--image", required=True, help="Path to input image (JPEG/PNG)")
parser.add_argument("--prompt", required=True, help="Text prompt")
parser.add_argument("--action_dir", default=None,
help="Directory containing poses.npy and intrinsics.npy")
parser.add_argument("--frame_num", type=int, default=81, help="Number of frames (default: 81)")
parser.add_argument("--size", default="480*832", help="Output size: 480*832 or 720*1280")
parser.add_argument("--seed", type=int, default=-1, help="RNG seed (-1 for random)")
parser.add_argument("--guide_scale", type=float, default=5.0)
parser.add_argument("--sampling_steps", type=int, default=40)
parser.add_argument("--poll_interval", type=int, default=10,
help="Seconds between status polls (default: 10)")
parser.add_argument("--output", default=None,
help="Local path to save downloaded video (default: <job_id>.mp4)")
args = parser.parse_args()
print(f" Prompt: {args.prompt[:80]}...")
print(f" Frames: {args.frame_num}, Size: {args.size}, Steps: {args.sampling_steps}")
payload = build_payload(args)
submit(args.url, payload, poll_interval=args.poll_interval, output_path=args.output)
if __name__ == "__main__":
main()