From 0242c0bb2250ed8bdc790a5c4e6b58a9b80afd1e Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 2 Feb 2026 19:18:57 +0000 Subject: [PATCH] Add stream command for streaming recipes - artdag stream runs streaming recipes - Supports --duration, --fps, --sources, --audio options - --wait flag polls for completion Co-Authored-By: Claude Opus 4.5 --- artdag.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/artdag.py b/artdag.py index 3e0d486..071e921 100755 --- a/artdag.py +++ b/artdag.py @@ -2245,5 +2245,105 @@ def run_status_v2(run_id): click.echo(f"Error: {run['error']}") +@cli.command("stream") +@click.argument("recipe_file", type=click.Path(exists=True)) +@click.option("--output", "-o", default="output.mp4", help="Output filename") +@click.option("--duration", "-d", type=float, help="Duration in seconds") +@click.option("--fps", type=float, help="FPS override") +@click.option("--sources", type=click.Path(exists=True), help="Sources config .sexp file") +@click.option("--audio", type=click.Path(exists=True), help="Audio config .sexp file") +@click.option("--wait", "-w", is_flag=True, help="Wait for completion") +def run_stream(recipe_file, output, duration, fps, sources, audio, wait): + """Run a streaming S-expression recipe. Requires login. + + RECIPE_FILE: Path to the recipe .sexp file + + Example: artdag stream effects/my_effect.sexp --duration 10 --fps 30 -w + """ + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + # Read recipe file + recipe_path = Path(recipe_file) + recipe_sexp = recipe_path.read_text() + + # Read optional config files + sources_sexp = None + if sources: + sources_sexp = Path(sources).read_text() + + audio_sexp = None + if audio: + audio_sexp = Path(audio).read_text() + + # Build request + request_data = { + "recipe_sexp": recipe_sexp, + "output_name": output, + } + if duration: + request_data["duration"] = duration + if fps: + request_data["fps"] = fps + if sources_sexp: + request_data["sources_sexp"] = sources_sexp + if audio_sexp: + request_data["audio_sexp"] = audio_sexp + + # Submit + try: + headers = get_auth_header(require_token=True) + resp = requests.post( + f"{get_server()}/runs/stream", + json=request_data, + headers=headers + ) + if resp.status_code == 401: + click.echo("Authentication failed. Please login again.", err=True) + sys.exit(1) + if resp.status_code == 400: + error = resp.json().get("detail", "Bad request") + click.echo(f"Error: {error}", err=True) + sys.exit(1) + resp.raise_for_status() + result = resp.json() + except requests.RequestException as e: + click.echo(f"Stream failed: {e}", err=True) + sys.exit(1) + + run_id = result["run_id"] + click.echo(f"Stream started: {run_id}") + click.echo(f"Task ID: {result.get('celery_task_id', 'N/A')}") + click.echo(f"Status: {result.get('status', 'pending')}") + + if wait: + click.echo("Waiting for completion...") + while True: + time.sleep(2) + try: + resp = requests.get( + f"{get_server()}/runs/{run_id}", + headers=get_auth_header() + ) + resp.raise_for_status() + run = resp.json() + except requests.RequestException: + continue + + status = run.get("status") + if status == "completed": + click.echo(f"\nCompleted!") + if run.get("output_cid"): + click.echo(f"Output CID: {run['output_cid']}") + break + elif status == "failed": + click.echo(f"\nFailed: {run.get('error', 'Unknown error')}", err=True) + sys.exit(1) + else: + click.echo(".", nl=False) + + if __name__ == "__main__": cli()